From ff35b2b4d60072e325faa9f7d3c2214ec7efe892 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sat, 13 Jun 2026 07:54:10 +0000 Subject: [PATCH 01/15] Integration --- docs/BUDGET_REPORT.md | 115 ++ docs/integration-guide.md | 2178 +++++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- 3 files changed, 2294 insertions(+), 1 deletion(-) diff --git a/docs/BUDGET_REPORT.md b/docs/BUDGET_REPORT.md index 251902a..5ac1e11 100644 --- a/docs/BUDGET_REPORT.md +++ b/docs/BUDGET_REPORT.md @@ -111,3 +111,118 @@ Any intentional breaking change must: 1. Update the relevant `compat_*` test to the new expectation. 2. Include a comment explaining why the break is necessary. 3. Bump the contract version in the PR. + + +# Soroban Budget Baseline Report + +Contract: **shipment** +SDK version: **soroban-sdk 22.0.0** +Captured: **2026-03-30** +Branch: `issue/197-195-budget-benchmark-schema-compat` + +--- + +## How to regenerate this report + +```sh +cargo test --package shipment budget_bench -- --nocapture 2>&1 \ + | grep '^\[budget\]' \ + | column -t +``` + +The test suite writes one `[budget]` line per operation to stdout. +Redirect output and paste the table below. + +--- + +## Baseline figures + +> **Note:** Run `cargo test --package shipment budget_bench -- --nocapture` +> and replace the placeholder values below with the actual output on first +> commit of this file. Values will vary slightly between machines; what +> matters is the *delta* between releases on the same machine. + +| Operation | CPU instructions | Memory bytes | +|-----------|-----------------|--------------| +| `initialize` | 88,650 | 9,505 | +| `create_shipment` (single) | 283,674 | 41,465 | +| `create_shipments_batch` (10 items) | 1,715,052 | 224,021 | +| `update_status` (Created → InTransit) | 333,039 | 51,307 | +| `deposit_escrow` | 298,576 | 46,942 | +| `confirm_delivery` (InTransit → Delivered) | 454,009 | 69,924 | +| `release_escrow` | 223,487 | 34,370 | +| `refund_escrow` | 370,226 | 52,106 | +| `raise_dispute` | 349,159 | 52,143 | +| `resolve_dispute` (RefundToCompany) | 396,119 | 58,810 | +| `record_milestone` (single) | 165,317 | 25,686 | +| `cancel_shipment` | 314,479 | 46,016 | +| `handoff_shipment` | 262,004 | 39,462 | + +--- + +## Network limits (Soroban v22) + +| Resource | Limit | +|----------|-------| +| CPU instructions per transaction | 100,000,000 | +| Memory bytes per transaction | 41,943,040 (40 MiB) | + +All operations must remain below these limits or the transaction is rejected +at the network level. + +--- + +## Interpreting budget deltas + +When comparing a new run against this baseline: + +| Delta | Action | +|-------|--------| +| ±5 % | Noise — no action needed | +| ±10 % | Review the diff of the hot-path function before merging | +| > +20 % | Must be explained in the PR description; consider optimisation | +| > +50 % | Block the merge; investigate and rewrite the hot path | + +### Common causes of CPU regressions + +- Adding a `Vec::iter()` loop inside a hot path (O(n) per call) +- Extra storage reads (`env.storage().get()`) per invocation +- New `env.events().publish()` calls (each costs ~10 k instructions) +- Increased `BytesN` or `Symbol` allocations + +### Common causes of memory regressions + +- Cloning large `Vec` or `Map` values instead of borrowing +- Building intermediate `Vec` just to discard them +- Storing large byte payloads on-chain (use the hash-and-emit pattern) + +--- + +## Re-run cadence + +Re-run and commit an updated table: + +1. Before cutting a release branch +2. After any change to `lib.rs`, `storage.rs`, or `events.rs` +3. After upgrading the `soroban-sdk` dependency + +--- + +## Schema compatibility + +Schema compatibility tests live in +[`contracts/shipment/src/schema_compat.rs`](../contracts/shipment/src/schema_compat.rs). + +They guard: +- `NavinError` discriminant values (31 variants, codes 1–31) +- `ShipmentStatus` FSM transition table +- All enum variant sets (`Role`, `BreachType`, `GeofenceEvent`, `DisputeResolution`, `NotificationType`, `AdminAction`) +- Struct field names and types (`Shipment`, `ShipmentInput`, `ContractMetadata`, `Analytics`) +- Event topic strings (`shipment_created`, `status_updated`, `escrow_deposited`, …) +- Public query function existence + +Any intentional breaking change must: + +1. Update the relevant `compat_*` test to the new expectation. +2. Include a comment explaining why the break is necessary. +3. Bump the contract version in the PR. diff --git a/docs/integration-guide.md b/docs/integration-guide.md index d358191..c9d1755 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -2174,3 +2174,2181 @@ describe("Shipment Integration", () => { ``` This integration guide provides complete TypeScript examples for interacting with the Navin shipment contract, including contract invocation, event listening, and transaction verification patterns that your Express backend can use. + + +# Navin Contract Integration Guide + +Complete guide for integrating the Navin shipment tracking smart contract with your Express.js backend using the Stellar JavaScript/TypeScript SDK. + +## Table of Contents +# Navin Contract Integration Guide + +Complete guide for integrating the Navin shipment tracking smart contract with your Express.js backend using the Stellar JavaScript/TypeScript SDK. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Contract Schema (ABI)](#contract-schema-abi) +3. [Event Payload Schemas](#event-payload-schemas) +4. [Setup & Configuration](#setup--configuration) +5. [Contract Invocation](#contract-invocation) +6. [Immutable Provenance Queries](#immutable-provenance-queries) +7. [Event Listening](#event-listening) +8. [Transaction Verification](#transaction-verification) +9. [Complete Examples](#complete-examples) + +## Contract Schema (ABI) + +The shipment contract publishes a machine-readable XDR interface schema at `docs/contract-schema.shipment.json`. This file is the authoritative source of truth for all function signatures, argument types, and return types exposed by the contract. Backend indexers and frontend clients should use it to deserialize call results and build typed invocations without manual reverse-engineering. + +### Regenerating the Schema + +Run the following command from the repository root after any contract change: + +```bash +make generate-schema-shipment +``` + +This will: +1. Build the contracts (`stellar contract build`) +2. Run `stellar contract info interface --wasm target/wasm32-unknown-unknown/release/shipment.wasm --output json-formatted` +3. Write the output to `docs/contract-schema.shipment.json` + +Commit the updated `docs/contract-schema.shipment.json` alongside your contract changes. + +### Backend Usage + +The schema JSON is an array of contract spec entries (functions, types, errors). Parse it to dynamically build decoders or validate call shapes: + +```typescript +import schema from '../docs/contract-schema.shipment.json'; +import { xdr, Contract } from '@stellar/stellar-sdk'; + +// Find a function entry +const fnEntry = schema.find( + (e: any) => e.function_v0?.name === 'create_shipment' +); + +// Use stellar-sdk's ContractSpec to decode return values +const spec = new Contract.Spec( + schema.map((e: any) => xdr.ScSpecEntry.fromJSON(e)) +); + +const decoded = spec.funcReturnType('get_shipment'); +``` + +Alternatively, use `stellar contract bindings typescript` to auto-generate a fully typed TypeScript client directly from the schema: + +```bash +stellar contract bindings typescript \ + --wasm target/wasm32-unknown-unknown/release/shipment.wasm \ + --output-dir src/generated/shipment-client +``` + +### Frontend Usage + +Import the generated TypeScript client or load the schema JSON at build time to drive type-safe contract calls: + +```typescript +// With generated bindings +import { Client as ShipmentClient } from './generated/shipment-client'; + +const client = new ShipmentClient({ + contractId: SHIPMENT_CONTRACT_ID, + networkPassphrase: Networks.TESTNET, + rpcUrl: RPC_URL, +}); + +const shipment = await client.get_shipment({ shipment_id: BigInt(1) }); +``` + +The schema also documents every `#[contracterror]` variant so frontend error-handling can display user-friendly messages for each code. + +### Schema Drift Enforcement in CI + +CI will fail if the committed `docs/contract-schema.shipment.json` diverges from the schema regenerated during the build. The `schema-drift` job in `.github/workflows/test.yml`: + +1. Builds the contracts from source +2. Regenerates the schema using the same `stellar contract info interface` command +3. Diffs the result against the committed file +4. Fails the workflow if any difference is found + +This ensures the schema file stays in sync with the contract code on every PR. If CI fails with a schema drift error, run `make generate-schema-shipment` locally, commit the updated JSON, and push again. + +## Event Payload Schemas + +Use `docs/event_schemas.md` as the central JSON-schema-like reference for emitted event payloads and field ordering. + +For quick contract client call examples, see [Client Examples](docs/client-examples.md). + +Indexer conformance fixtures are validated by tests in: + +- `contracts/shipment/src/test_event_fixtures.rs` +- `contracts/shipment/test_snapshots/test_event_fixtures/` + +When parser logic changes, regenerate and re-validate against these fixture outputs before release. + +## Architecture Overview + +Navin uses a **Hash-and-Emit** architecture: + +- **On-chain**: Contract stores only critical data (shipment IDs, addresses, status, escrow amounts) and emits events with data hashes +- **Off-chain**: Backend (MongoDB) stores full shipment details (GPS coordinates, sensor readings, photos, metadata) +- **Verification**: Data integrity is verified by comparing on-chain hashes with off-chain data hashes + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Frontend │────────▶│ Express Backend │────────▶│ Stellar Network │ +│ (React) │ │ (Indexer) │ │ (Soroban) │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ + │ │ + ▼ ▼ + ┌──────────────────┐ ┌─────────────────┐ + │ MongoDB │ │ Event Stream │ + │ (Full Data) │◀────────│ (Horizon) │ + └──────────────────┘ └─────────────────┘ +``` + +## Setup & Configuration + +### Installation + +```bash +npm install @stellar/stellar-sdk +# or +yarn add @stellar/stellar-sdk +``` + +### Environment Configuration + +Create a configuration file for network settings: + +```typescript +// src/config/stellar.config.ts +``` + +export interface StellarConfig { +networkPassphrase: string; +rpcUrl: string; +horizonUrl: string; +contractId: string; +sourceSecretKey: string; +} + +export const testnetConfig: StellarConfig = { +networkPassphrase: 'Test SDF Network ; September 2015', +rpcUrl: 'https://soroban-testnet.stellar.org:443', +horizonUrl: 'https://horizon-testnet.stellar.org', +contractId: process.env.SHIPMENT_CONTRACT_ID!, +sourceSecretKey: process.env.STELLAR_SECRET_KEY!, +}; + +export const mainnetConfig: StellarConfig = { +networkPassphrase: 'Public Global Stellar Network ; September 2015', +rpcUrl: 'https://soroban-rpc.stellar.org:443', +horizonUrl: 'https://horizon.stellar.org', +contractId: process.env.SHIPMENT_CONTRACT_ID!, +sourceSecretKey: process.env.STELLAR_SECRET_KEY!, +}; + +// Select config based on environment +export const config = process.env.NODE_ENV === 'production' +? mainnetConfig +: testnetConfig; + +```` + +### Initialize Stellar SDK + +```typescript +// src/services/stellar.service.ts +import { + SorobanRpc, + Keypair, + Contract, + TransactionBuilder, + Networks, + Operation, + BASE_FEE, + Address, + xdr, + scValToNative, + nativeToScVal, +} from '@stellar/stellar-sdk'; +import { config } from '../config/stellar.config'; + +export class StellarService { + private server: SorobanRpc.Server; + private sourceKeypair: Keypair; + private contract: Contract; + + constructor() { + this.server = new SorobanRpc.Server(config.rpcUrl); + this.sourceKeypair = Keypair.fromSecret(config.sourceSecretKey); + this.contract = new Contract(config.contractId); + } + + async getAccount() { + return await this.server.getAccount(this.sourceKeypair.publicKey()); + } +} +```` + +## Contract Invocation + +### Example 1: Create a Shipment + +```typescript +// src/services/shipment.service.ts +``` + +import { StellarService } from './stellar.service'; +import { createHash } from 'crypto'; + +export class ShipmentService extends StellarService { + +/\*\* + +- Create a new shipment on-chain + \*/ + async createShipment(shipmentData: { + sender: string; + receiver: string; + carrier: string; + offChainData: any; + paymentMilestones: Array<{ checkpoint: string; percentage: number }>; + deadline: Date; + }) { + try { + // 1. Hash the off-chain data + const dataHash = this.hashOffChainData(shipmentData.offChainData); + + // 2. Prepare contract arguments + const milestones = shipmentData.paymentMilestones.map(m => [ + m.checkpoint, + m.percentage + ]); + + // 3. Build transaction + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'create_shipment', + Address.fromString(shipmentData.sender), + Address.fromString(shipmentData.receiver), + Address.fromString(shipmentData.carrier), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }), + nativeToScVal(milestones, { type: 'vec' }), + nativeToScVal(Math.floor(shipmentData.deadline.getTime() / 1000), { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + // 4. Sign and submit + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + // 5. Wait for confirmation + if (response.status === 'PENDING') { + const result = await this.server.getTransaction(response.hash); + return { + success: true, + txHash: response.hash, + shipmentId: this.extractShipmentIdFromResult(result), + dataHash + }; + } + + throw new Error(`Transaction failed: ${response.status}`); + + } catch (error) { + console.error('Failed to create shipment:', error); + throw error; + } + } + +/\*\* + +- Update shipment status + \*/ + async updateShipmentStatus( + caller: string, + shipmentId: number, + newStatus: string, + offChainData: any + ) { + const dataHash = this.hashOffChainData(offChainData); + + + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'update_status', + Address.fromString(caller), + nativeToScVal(shipmentId, { type: 'u64' }), + nativeToScVal(newStatus, { type: 'symbol' }), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }) + ) + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + return { + success: response.status === 'SUCCESS', + txHash: response.hash, + dataHash + }; + +} + +/\*\* + +- Record milestone for shipment + \*/ + async recordMilestone( + carrier: string, + shipmentId: number, + checkpoint: string, + offChainData: any + ) { + const dataHash = this.hashOffChainData(offChainData); + + + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'record_milestone', + Address.fromString(carrier), + nativeToScVal(shipmentId, { type: 'u64' }), + nativeToScVal(checkpoint, { type: 'symbol' }), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }) + ) + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + return { + success: response.status === 'SUCCESS', + txHash: response.hash, + dataHash + }; + +} + +private hashOffChainData(data: any): string { +const jsonString = JSON.stringify(data, Object.keys(data).sort()); +return createHash('sha256').update(jsonString).digest('hex'); +} + +private extractShipmentIdFromResult(result: any): number { +// Extract shipment ID from transaction result +// Implementation depends on Stellar SDK response format +return scValToNative(result.returnValue); +} +} + +```` + +## Immutable Provenance Queries + +Use these read-only methods to render verification views without fetching the full shipment payload. + +- `get_shipment_creator(shipment_id: u64) -> Address`: Returns the immutable creator (sender) address captured at shipment creation. +- `get_shipment_receiver(shipment_id: u64) -> Address`: Returns the immutable receiver address captured at shipment creation. + +Both methods fail with `ShipmentNotFound` for unknown IDs, which helps frontend verification flows distinguish missing records from mismatched identities. + +### Restore Triage Query + +For pre-restore operations, call: + +- `get_restore_diagnostics(shipment_id: u64) -> PersistentRestoreDiagnostics` + +Interpretation guidance: + +- `ActivePersistent`: no restore path needed for the primary shipment payload. +- `ArchivedExpected`: shipment has been archived and may require restore flow depending on operator policy. +- `Missing`: shipment ID is absent from both persistent and archived paths. +- `InconsistentDualPresence`: both paths are populated; investigate before any restore mutation. + +This method is read-only and does not mutate contract state. + +```typescript +// src/services/shipment-query.service.ts +import { + Contract, + TransactionBuilder, + BASE_FEE, + Address, + nativeToScVal, +} from '@stellar/stellar-sdk'; +import { StellarService } from './stellar.service'; +import { config } from '../config/stellar.config'; + +export class ShipmentQueryService extends StellarService { + async getShipmentCreator(shipmentId: number): Promise { + const contract = new Contract(config.contractId); + const account = await this.getAccount(); + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + contract.call( + 'get_shipment_creator', + nativeToScVal(shipmentId, { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + const sim = await this.server.simulateTransaction(tx); + return Address.fromScVal(sim.result!.retval).toString(); + } + + async getShipmentReceiver(shipmentId: number): Promise { + const contract = new Contract(config.contractId); + const account = await this.getAccount(); + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + contract.call( + 'get_shipment_receiver', + nativeToScVal(shipmentId, { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + const sim = await this.server.simulateTransaction(tx); + return Address.fromScVal(sim.result!.retval).toString(); + } +} +``` + +## Event Listening + +### Horizon Event Stream Listener + +```typescript +// src/services/event-listener.service.ts +import { Server } from '@stellar/stellar-sdk/lib/horizon'; +import { config } from '../config/stellar.config'; + +export class EventListenerService { + private horizonServer: Server; + + constructor() { + this.horizonServer = new Server(config.horizonUrl); + } + + /** + * Listen for contract events for a specific shipment + */ + async listenForShipmentEvents(shipmentId: number, callback: (event: any) => void) { + const eventStream = this.horizonServer + .effects() + .forAccount(config.contractId) + .cursor('now') + .stream({ + onmessage: (effect) => { + if (this.isShipmentEvent(effect, shipmentId)) { + callback(this.parseContractEvent(effect)); + } + }, + onerror: (error) => { + console.error('Event stream error:', error); + } + }); + + return eventStream; + } + + /** + * Listen for all contract events + */ + async listenForAllEvents(callback: (event: any) => void) { + const eventStream = this.horizonServer + .effects() + .forAccount(config.contractId) + .cursor('now') + .stream({ + onmessage: (effect) => { + if (effect.type === 'contract_credited' || effect.type === 'contract_debited') { + const event = this.parseContractEvent(effect); + if (event) { + callback(event); + } + } + }, + onerror: (error) => { + console.error('Event stream error:', error); + } + }); + + return eventStream; + } + + private isShipmentEvent(effect: any, shipmentId: number): boolean { + // Check if the event relates to the specific shipment + const event = this.parseContractEvent(effect); + return event && event.shipmentId === shipmentId; + } + + private parseContractEvent(effect: any): any { + try { + // Parse the contract event from Horizon effect + // This is a simplified example - actual parsing depends on event structure + const eventData = effect.data; + + return { + type: eventData.topic?.[0], + shipmentId: eventData.data?.[0], + timestamp: effect.created_at, + txHash: effect.transaction_hash, + ...eventData.data + }; + } catch (error) { + console.error('Failed to parse contract event:', error); + return null; + } + } +} +```` + +### Event Processing Service + +```typescript +// src/services/event-processor.service.ts +import { EventListenerService } from "./event-listener.service"; +import { ShipmentModel } from "../models/shipment.model"; + +export class EventProcessorService { + private eventListener: EventListenerService; + + constructor() { + this.eventListener = new EventListenerService(); + } + + async startProcessing() { + await this.eventListener.listenForAllEvents(async (event) => { + try { + await this.processEvent(event); + } catch (error) { + console.error("Failed to process event:", error); + } + }); + } + + private async processEvent(event: any) { + switch (event.type) { + case "shipment_created": + await this.handleShipmentCreated(event); + break; + case "status_updated": + await this.handleStatusUpdated(event); + break; + case "milestone_recorded": + await this.handleMilestoneRecorded(event); + break; + case "escrow_deposited": + await this.handleEscrowDeposited(event); + break; + case "escrow_released": + await this.handleEscrowReleased(event); + break; + default: + console.log("Unknown event type:", event.type); + } + } + + private async handleShipmentCreated(event: any) { + // Update MongoDB with new shipment + await ShipmentModel.create({ + shipmentId: event.shipmentId, + sender: event.sender, + receiver: event.receiver, + dataHash: event.dataHash, + status: "Created", + createdAt: new Date(event.timestamp), + txHash: event.txHash, + }); + + console.log(`Shipment ${event.shipmentId} created`); + } + + private async handleStatusUpdated(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + status: event.newStatus, + dataHash: event.dataHash, + updatedAt: new Date(event.timestamp), + lastTxHash: event.txHash, + }, + ); + + console.log( + `Shipment ${event.shipmentId} status updated to ${event.newStatus}`, + ); + } + + private async handleMilestoneRecorded(event: any) { + // Add milestone to shipment record + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + $push: { + milestones: { + checkpoint: event.checkpoint, + dataHash: event.dataHash, + timestamp: new Date(event.timestamp), + reporter: event.reporter, + txHash: event.txHash, + }, + }, + }, + ); + + console.log( + `Milestone ${event.checkpoint} recorded for shipment ${event.shipmentId}`, + ); + } + + private async handleEscrowDeposited(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + escrowAmount: event.amount, + escrowTxHash: event.txHash, + }, + ); + } + + private async handleEscrowReleased(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + $inc: { escrowAmount: -event.amount }, + releaseTxHash: event.txHash, + }, + ); + } +} +``` + +## Transaction Verification + +### Verify Transaction Hash and Data Integrity + +```typescript +// src/services/verification.service.ts +import { StellarService } from "./stellar.service"; +import { ShipmentModel } from "../models/shipment.model"; +import { createHash } from "crypto"; + +export class VerificationService extends StellarService { + /** + * Verify transaction hash exists on-chain and compare data hash + */ + async verifyTransaction( + txHash: string, + expectedDataHash: string, + ): Promise<{ + valid: boolean; + onChain: boolean; + dataMatch: boolean; + details?: any; + }> { + try { + // 1. Get transaction from Stellar network + const transaction = await this.server.getTransaction(txHash); + + if (!transaction) { + return { + valid: false, + onChain: false, + dataMatch: false, + }; + } + + // 2. Extract data hash from transaction + const onChainDataHash = this.extractDataHashFromTransaction(transaction); + + // 3. Compare hashes + const dataMatch = onChainDataHash === expectedDataHash; + + return { + valid: transaction.successful && dataMatch, + onChain: true, + dataMatch, + details: { + ledger: transaction.ledger, + createdAt: transaction.created_at, + fee: transaction.fee_charged, + onChainDataHash, + expectedDataHash, + }, + }; + } catch (error) { + console.error("Transaction verification failed:", error); + return { + valid: false, + onChain: false, + dataMatch: false, + }; + } + } + + /** + * Verify shipment data integrity between MongoDB and blockchain + */ + async verifyShipmentIntegrity(shipmentId: number): Promise<{ + valid: boolean; + issues: string[]; + }> { + const issues: string[] = []; + + try { + // 1. Get shipment from MongoDB + const dbShipment = await ShipmentModel.findOne({ shipmentId }); + if (!dbShipment) { + issues.push("Shipment not found in database"); + return { valid: false, issues }; + } + + // 2. Get shipment from blockchain + const onChainShipment = await this.getShipmentFromChain(shipmentId); + if (!onChainShipment) { + issues.push("Shipment not found on blockchain"); + return { valid: false, issues }; + } + + // 3. Compare critical fields + if (dbShipment.sender !== onChainShipment.sender) { + issues.push("Sender mismatch"); + } + + if (dbShipment.receiver !== onChainShipment.receiver) { + issues.push("Receiver mismatch"); + } + + if (dbShipment.status !== onChainShipment.status) { + issues.push("Status mismatch"); + } + + // 4. Verify data hash + const computedHash = this.hashOffChainData(dbShipment.fullData); + if (computedHash !== onChainShipment.dataHash) { + issues.push("Data hash mismatch - data may be corrupted"); + } + + // 5. Verify transaction hashes + if (dbShipment.txHash) { + const txVerification = await this.verifyTransaction( + dbShipment.txHash, + dbShipment.dataHash, + ); + if (!txVerification.valid) { + issues.push("Creation transaction verification failed"); + } + } + + return { + valid: issues.length === 0, + issues, + }; + } catch (error) { + console.error("Integrity verification failed:", error); + return { + valid: false, + issues: ["Verification process failed"], + }; + } + } + + private async getShipmentFromChain(shipmentId: number) { + try { + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + "get_shipment", + nativeToScVal(shipmentId, { type: "u64" }), + ), + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + if (response.status === "SUCCESS") { + return scValToNative(response.returnValue); + } + + return null; + } catch (error) { + console.error("Failed to get shipment from chain:", error); + return null; + } + } + + private extractDataHashFromTransaction(transaction: any): string { + // Extract data hash from transaction operations + // Implementation depends on transaction structure + try { + const operation = transaction.operations[0]; + // Parse the operation to extract data hash + return operation.parameters?.data_hash || ""; + } catch (error) { + console.error("Failed to extract data hash:", error); + return ""; + } + } + + private hashOffChainData(data: any): string { + const jsonString = JSON.stringify(data, Object.keys(data).sort()); + return createHash("sha256").update(jsonString).digest("hex"); + } +} +``` + +## Complete Examples + +### Express.js Route Implementation + +```typescript +// src/routes/shipments.ts +import { Router } from "express"; +import { ShipmentService } from "../services/shipment.service"; +import { VerificationService } from "../services/verification.service"; + +const router = Router(); +const shipmentService = new ShipmentService(); +const verificationService = new VerificationService(); + +// Create shipment +router.post("/shipments", async (req, res) => { + try { + const { + sender, + receiver, + carrier, + shipmentData, + paymentMilestones, + deadline, + } = req.body; + + const result = await shipmentService.createShipment({ + sender, + receiver, + carrier, + offChainData: shipmentData, + paymentMilestones, + deadline: new Date(deadline), + }); + + res.json({ + success: true, + shipmentId: result.shipmentId, + txHash: result.txHash, + dataHash: result.dataHash, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +// Update shipment status +router.put("/shipments/:id/status", async (req, res) => { + try { + const { id } = req.params; + const { caller, newStatus, updateData } = req.body; + + const result = await shipmentService.updateShipmentStatus( + caller, + parseInt(id), + newStatus, + updateData, + ); + + res.json(result); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +// Verify transaction +router.get("/verify/:txHash", async (req, res) => { + try { + const { txHash } = req.params; + const { expectedDataHash } = req.query; + + const verification = await verificationService.verifyTransaction( + txHash, + expectedDataHash as string, + ); + + res.json(verification); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +export default router; +``` + +### MongoDB Schema + +```typescript +// src/models/shipment.model.ts +import mongoose from "mongoose"; + +const milestoneSchema = new mongoose.Schema({ + checkpoint: String, + dataHash: String, + timestamp: Date, + reporter: String, + txHash: String, + gpsCoordinates: { + latitude: Number, + longitude: Number, + }, + sensorData: { + temperature: Number, + humidity: Number, + pressure: Number, + }, +}); + +const shipmentSchema = new mongoose.Schema({ + shipmentId: { type: Number, unique: true, required: true }, + sender: { type: String, required: true }, + receiver: { type: String, required: true }, + carrier: { type: String, required: true }, + status: { type: String, required: true }, + dataHash: { type: String, required: true }, + txHash: String, + createdAt: Date, + updatedAt: Date, + deadline: Date, + escrowAmount: Number, + milestones: [milestoneSchema], + fullData: { + description: String, + weight: Number, + dimensions: { + length: Number, + width: Number, + height: Number, + }, + specialInstructions: String, + photos: [String], + documents: [String], + }, +}); + +export const ShipmentModel = mongoose.model("Shipment", shipmentSchema); +``` + +### Environment Variables + +```bash +# .env +NODE_ENV=development +STELLAR_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +SHIPMENT_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +TOKEN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +MONGODB_URI=mongodb://localhost:27017/navin +``` + +## Best Practices + +1. **Error Handling**: Always wrap Stellar operations in try-catch blocks +2. **Rate Limiting**: Implement rate limiting for contract calls to avoid hitting network limits +3. **Data Validation**: Validate all input data before creating hashes or submitting transactions +4. **Event Deduplication**: Handle duplicate events that may occur during network issues +5. **Transaction Fees**: Monitor and adjust transaction fees based on network conditions +6. **Security**: Never expose private keys in client-side code or logs + + +## Error Code Mapping + +Every contract invocation that fails returns a Soroban `ContractError` whose numeric code maps to a `NavinError` variant. The authoritative mapping — including user-facing category and retry guidance — lives in +[`contracts/shipment/src/error_map.rs`](../contracts/shipment/src/error_map.rs). + +### Categories + +| Category | Meaning | +|---|---| +| `InvalidInput` | Caller supplied bad data; fix the request before resubmitting. | +| `Unauthorized` | Missing role or signature; check auth setup. | +| `NotFound` | Referenced resource does not exist. | +| `InvalidState` | Operation not allowed in the current shipment state. | +| `LimitExceeded` | A resource cap or rate limit was hit. | +| `Transient` | Infrastructure or arithmetic failure; may resolve on retry. | +| `Configuration` | Contract initialisation or config problem. | + +### Retry Guidance + +| Guidance | Action | +|---|---| +| `NoRetry` | Do not retry; the request must be corrected first. | +| `RetryAfterDelay` | Retry after a short back-off (transient / rate-limit). | +| `RetryAfterStateChange` | Retry only after the relevant on-chain state changes. | + +### Quick Reference Table + +| Code | Variant | Category | Retry | +|---|---|---|---| +| 1 | `AlreadyInitialized` | Configuration | NoRetry | +| 2 | `NotInitialized` | Configuration | NoRetry | +| 3 | `Unauthorized` | Unauthorized | NoRetry | +| 4 | `ShipmentNotFound` | NotFound | NoRetry | +| 5 | `InvalidStatus` | InvalidState | RetryAfterStateChange | +| 6 | `InvalidHash` | InvalidInput | NoRetry | +| 7 | `EscrowLocked` | InvalidState | RetryAfterStateChange | +| 8 | `InsufficientFunds` | InvalidInput | NoRetry | +| 9 | `ShipmentAlreadyCompleted` | InvalidState | NoRetry | +| 10 | `InvalidTimestamp` | InvalidInput | NoRetry | +| 11 | `CounterOverflow` | Transient | NoRetry | +| 14 | `InvalidAmount` | InvalidInput | NoRetry | +| 15 | `EscrowAlreadyDeposited` | InvalidState | NoRetry | +| 16 | `BatchTooLarge` | LimitExceeded | NoRetry | +| 17 | `InvalidShipmentInput` | InvalidInput | NoRetry | +| 18 | `MilestoneSumInvalid` | InvalidInput | NoRetry | +| 19 | `MilestoneAlreadyPaid` | InvalidState | NoRetry | +| 20 | `MetadataLimitExceeded` | LimitExceeded | NoRetry | +| 21 | `RateLimitExceeded` | LimitExceeded | RetryAfterDelay | +| 22 | `ProposalNotFound` | NotFound | NoRetry | +| 23 | `ProposalAlreadyExecuted` | InvalidState | NoRetry | +| 24 | `ProposalExpired` | InvalidState | NoRetry | +| 25 | `AlreadyApproved` | InvalidState | NoRetry | +| 26 | `InsufficientApprovals` | InvalidState | RetryAfterStateChange | +| 27 | `NotAnAdmin` | Unauthorized | NoRetry | +| 28 | `InvalidMultiSigConfig` | InvalidInput | NoRetry | +| 29 | `NotExpired` | InvalidState | RetryAfterStateChange | +| 30 | `ShipmentLimitReached` | LimitExceeded | RetryAfterStateChange | +| 31 | `InvalidConfig` | InvalidInput | NoRetry | +| 32 | `CannotSelfRevoke` | InvalidInput | NoRetry | +| 33 | `CarrierSuspended` | Unauthorized | RetryAfterStateChange | +| 34 | `ForceCancelReasonHashMissing` | InvalidInput | NoRetry | +| 35 | `ArithmeticError` | Transient | NoRetry | +| 36 | `DisputeReasonHashMissing` | InvalidInput | NoRetry | +| 37 | `CompanySuspended` | Unauthorized | RetryAfterStateChange | +| 38 | `ShipmentFinalized` | InvalidState | NoRetry | +| 39 | `TokenTransferFailed` | Transient | RetryAfterDelay | +| 40 | `TokenMintFailed` | Transient | RetryAfterDelay | +| 41 | `DuplicateAction` | InvalidInput | NoRetry | +| 42 | `ShipmentUnavailable` | InvalidState | RetryAfterStateChange | +| 43 | `ContractPaused` | InvalidState | RetryAfterStateChange | +| 44 | `StatusHashNotFound` | NotFound | NoRetry | +| 45 | `DataHashMismatch` | InvalidInput | NoRetry | +| 46 | `CircuitBreakerOpen` | Transient | RetryAfterDelay | +| 47 | `InvalidMigrationEdge` | InvalidInput | NoRetry | +| 48 | `MilestoneLimitExceeded` | LimitExceeded | NoRetry | +| 49 | `NoteLimitExceeded` | LimitExceeded | NoRetry | +| 50 | `EvidenceLimitExceeded` | LimitExceeded | NoRetry | +| 51 | `BreachLimitExceeded` | LimitExceeded | NoRetry | +| 52 | `InvalidTokenDecimals` | InvalidInput | NoRetry | + +### TypeScript Usage + +```typescript +const ERROR_MAP: Record = { + 1: { category: "Configuration", retry: "NoRetry", message: "Contract is already initialised; call init only once." }, + 2: { category: "Configuration", retry: "NoRetry", message: "Contract has not been initialised; call init first." }, + 3: { category: "Unauthorized", retry: "NoRetry", message: "Caller does not hold the required role or signature." }, + 4: { category: "NotFound", retry: "NoRetry", message: "Shipment ID does not exist." }, + 5: { category: "InvalidState", retry: "RetryAfterStateChange", message: "State transition not allowed from current status." }, + 6: { category: "InvalidInput", retry: "NoRetry", message: "Provided data hash does not match the stored value." }, + 7: { category: "InvalidState", retry: "RetryAfterStateChange", message: "Escrow is locked; wait for terminal state." }, + 8: { category: "InvalidInput", retry: "NoRetry", message: "Caller balance too low for escrow deposit." }, + 9: { category: "InvalidState", retry: "NoRetry", message: "Shipment already in a terminal state." }, + 21: { category: "LimitExceeded", retry: "RetryAfterDelay", message: "Rate limit hit; retry after the interval elapses." }, + 39: { category: "Transient", retry: "RetryAfterDelay", message: "Token transfer failed; retry after verifying token state." }, + 43: { category: "InvalidState", retry: "RetryAfterStateChange", message: "Contract is paused; wait for operator to resume." }, + 46: { category: "Transient", retry: "RetryAfterDelay", message: "Circuit breaker open; token transfers temporarily disabled." }, + // … see error_map.rs for the full list +}; + +function handleContractError(code: number): void { + const info = ERROR_MAP[code]; + if (!info) { + console.error(`Unknown contract error code: ${code}`); + return; + } + console.error(`[${info.category}] ${info.message}`); + if (info.retry === "RetryAfterDelay") { + scheduleRetry(); + } else if (info.retry === "RetryAfterStateChange") { + waitForStateChange(); + } + // NoRetry → surface error to the user +} +``` + +## Testing + +```typescript +// src/tests/shipment.test.ts +import { ShipmentService } from "../services/shipment.service"; +import { VerificationService } from "../services/verification.service"; + +describe("Shipment Integration", () => { + let shipmentService: ShipmentService; + let verificationService: VerificationService; + + beforeEach(() => { + shipmentService = new ShipmentService(); + verificationService = new VerificationService(); + }); + + it("should create shipment and verify transaction", async () => { + const shipmentData = { + sender: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + receiver: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + carrier: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + offChainData: { + description: "Test shipment", + weight: 10.5, + specialInstructions: "Handle with care", + }, + paymentMilestones: [ + { checkpoint: "pickup", percentage: 30 }, + { checkpoint: "delivery", percentage: 70 }, + ], + deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }; + + const result = await shipmentService.createShipment(shipmentData); + expect(result.success).toBe(true); + expect(result.shipmentId).toBeGreaterThan(0); + expect(result.txHash).toBeDefined(); + + // Verify the transaction + const verification = await verificationService.verifyTransaction( + result.txHash, + result.dataHash, + ); + expect(verification.valid).toBe(true); + expect(verification.onChain).toBe(true); + expect(verification.dataMatch).toBe(true); + }); +}); +``` + +This integration guide provides complete TypeScript examples for interacting with the Navin shipment contract, including contract invocation, event listening, and transaction verification patterns that your Express backend can use. + +1. [Architecture Overview](#architecture-overview) +2. [Setup & Configuration](#setup--configuration) +3. [Contract Invocation](#contract-invocation) +4. [Immutable Provenance Queries](#immutable-provenance-queries) +5. [Event Listening](#event-listening) +6. [Transaction Verification](#transaction-verification) +7. [Complete Examples](#complete-examples) + +## Architecture Overview + +Navin uses a **Hash-and-Emit** architecture: + +- **On-chain**: Contract stores only critical data (shipment IDs, addresses, status, escrow amounts) and emits events with data hashes +- **Off-chain**: Backend (MongoDB) stores full shipment details (GPS coordinates, sensor readings, photos, metadata) +- **Verification**: Data integrity is verified by comparing on-chain hashes with off-chain data hashes + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Frontend │────────▶│ Express Backend │────────▶│ Stellar Network │ +│ (React) │ │ (Indexer) │ │ (Soroban) │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ + │ │ + ▼ ▼ + ┌──────────────────┐ ┌─────────────────┐ + │ MongoDB │ │ Event Stream │ + │ (Full Data) │◀────────│ (Horizon) │ + └──────────────────┘ └─────────────────┘ +``` + +## Setup & Configuration + +### Installation + +```bash +npm install @stellar/stellar-sdk +# or +yarn add @stellar/stellar-sdk +``` + +### Environment Configuration + +Create a configuration file for network settings: + +```typescript +// src/config/stellar.config.ts +``` + +export interface StellarConfig { +networkPassphrase: string; +rpcUrl: string; +horizonUrl: string; +contractId: string; +sourceSecretKey: string; +} + +export const testnetConfig: StellarConfig = { +networkPassphrase: 'Test SDF Network ; September 2015', +rpcUrl: 'https://soroban-testnet.stellar.org:443', +horizonUrl: 'https://horizon-testnet.stellar.org', +contractId: process.env.SHIPMENT_CONTRACT_ID!, +sourceSecretKey: process.env.STELLAR_SECRET_KEY!, +}; + +export const mainnetConfig: StellarConfig = { +networkPassphrase: 'Public Global Stellar Network ; September 2015', +rpcUrl: 'https://soroban-rpc.stellar.org:443', +horizonUrl: 'https://horizon.stellar.org', +contractId: process.env.SHIPMENT_CONTRACT_ID!, +sourceSecretKey: process.env.STELLAR_SECRET_KEY!, +}; + +// Select config based on environment +export const config = process.env.NODE_ENV === 'production' +? mainnetConfig +: testnetConfig; + +```` + +### Initialize Stellar SDK + +```typescript +// src/services/stellar.service.ts +import { + SorobanRpc, + Keypair, + Contract, + TransactionBuilder, + Networks, + Operation, + BASE_FEE, + Address, + xdr, + scValToNative, + nativeToScVal, +} from '@stellar/stellar-sdk'; +import { config } from '../config/stellar.config'; + +export class StellarService { + private server: SorobanRpc.Server; + private sourceKeypair: Keypair; + private contract: Contract; + + constructor() { + this.server = new SorobanRpc.Server(config.rpcUrl); + this.sourceKeypair = Keypair.fromSecret(config.sourceSecretKey); + this.contract = new Contract(config.contractId); + } + + async getAccount() { + return await this.server.getAccount(this.sourceKeypair.publicKey()); + } +} +```` + +## Contract Invocation + +### Example 1: Create a Shipment + +```typescript +// src/services/shipment.service.ts +``` + +import { StellarService } from './stellar.service'; +import { createHash } from 'crypto'; + +export class ShipmentService extends StellarService { + +/\*\* + +- Create a new shipment on-chain + \*/ + async createShipment(shipmentData: { + sender: string; + receiver: string; + carrier: string; + offChainData: any; + paymentMilestones: Array<{ checkpoint: string; percentage: number }>; + deadline: Date; + }) { + try { + // 1. Hash the off-chain data + const dataHash = this.hashOffChainData(shipmentData.offChainData); + + // 2. Prepare contract arguments + const milestones = shipmentData.paymentMilestones.map(m => [ + m.checkpoint, + m.percentage + ]); + + // 3. Build transaction + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'create_shipment', + Address.fromString(shipmentData.sender), + Address.fromString(shipmentData.receiver), + Address.fromString(shipmentData.carrier), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }), + nativeToScVal(milestones, { type: 'vec' }), + nativeToScVal(Math.floor(shipmentData.deadline.getTime() / 1000), { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + // 4. Sign and submit + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + // 5. Wait for confirmation + if (response.status === 'PENDING') { + const result = await this.server.getTransaction(response.hash); + return { + success: true, + txHash: response.hash, + shipmentId: this.extractShipmentIdFromResult(result), + dataHash + }; + } + + throw new Error(`Transaction failed: ${response.status}`); + + } catch (error) { + console.error('Failed to create shipment:', error); + throw error; + } + } + +/\*\* + +- Update shipment status + \*/ + async updateShipmentStatus( + caller: string, + shipmentId: number, + newStatus: string, + offChainData: any + ) { + const dataHash = this.hashOffChainData(offChainData); + + + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'update_status', + Address.fromString(caller), + nativeToScVal(shipmentId, { type: 'u64' }), + nativeToScVal(newStatus, { type: 'symbol' }), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }) + ) + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + return { + success: response.status === 'SUCCESS', + txHash: response.hash, + dataHash + }; + +} + +/\*\* + +- Record milestone for shipment + \*/ + async recordMilestone( + carrier: string, + shipmentId: number, + checkpoint: string, + offChainData: any + ) { + const dataHash = this.hashOffChainData(offChainData); + + + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + 'record_milestone', + Address.fromString(carrier), + nativeToScVal(shipmentId, { type: 'u64' }), + nativeToScVal(checkpoint, { type: 'symbol' }), + nativeToScVal(Buffer.from(dataHash, 'hex'), { type: 'bytes' }) + ) + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + return { + success: response.status === 'SUCCESS', + txHash: response.hash, + dataHash + }; + +} + +private hashOffChainData(data: any): string { +const jsonString = JSON.stringify(data, Object.keys(data).sort()); +return createHash('sha256').update(jsonString).digest('hex'); +} + +private extractShipmentIdFromResult(result: any): number { +// Extract shipment ID from transaction result +// Implementation depends on Stellar SDK response format +return scValToNative(result.returnValue); +} +} + +```` + +## Immutable Provenance Queries + +Use these read-only methods to render verification views without fetching the full shipment payload. + +- `get_shipment_creator(shipment_id: u64) -> Address`: Returns the immutable creator (sender) address captured at shipment creation. +- `get_shipment_receiver(shipment_id: u64) -> Address`: Returns the immutable receiver address captured at shipment creation. + +Both methods fail with `ShipmentNotFound` for unknown IDs, which helps frontend verification flows distinguish missing records from mismatched identities. + +### Restore Triage Query + +For pre-restore operations, call: + +- `get_restore_diagnostics(shipment_id: u64) -> PersistentRestoreDiagnostics` + +Interpretation guidance: + +- `ActivePersistent`: no restore path needed for the primary shipment payload. +- `ArchivedExpected`: shipment has been archived and may require restore flow depending on operator policy. +- `Missing`: shipment ID is absent from both persistent and archived paths. +- `InconsistentDualPresence`: both paths are populated; investigate before any restore mutation. + +This method is read-only and does not mutate contract state. + +```typescript +// src/services/shipment-query.service.ts +import { + Contract, + TransactionBuilder, + BASE_FEE, + Address, + nativeToScVal, +} from '@stellar/stellar-sdk'; +import { StellarService } from './stellar.service'; +import { config } from '../config/stellar.config'; + +export class ShipmentQueryService extends StellarService { + async getShipmentCreator(shipmentId: number): Promise { + const contract = new Contract(config.contractId); + const account = await this.getAccount(); + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + contract.call( + 'get_shipment_creator', + nativeToScVal(shipmentId, { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + const sim = await this.server.simulateTransaction(tx); + return Address.fromScVal(sim.result!.retval).toString(); + } + + async getShipmentReceiver(shipmentId: number): Promise { + const contract = new Contract(config.contractId); + const account = await this.getAccount(); + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + contract.call( + 'get_shipment_receiver', + nativeToScVal(shipmentId, { type: 'u64' }) + ) + ) + .setTimeout(30) + .build(); + + const sim = await this.server.simulateTransaction(tx); + return Address.fromScVal(sim.result!.retval).toString(); + } +} +``` + +## Event Listening + +### Horizon Event Stream Listener + +```typescript +// src/services/event-listener.service.ts +import { Server } from '@stellar/stellar-sdk/lib/horizon'; +import { config } from '../config/stellar.config'; + +export class EventListenerService { + private horizonServer: Server; + + constructor() { + this.horizonServer = new Server(config.horizonUrl); + } + + /** + * Listen for contract events for a specific shipment + */ + async listenForShipmentEvents(shipmentId: number, callback: (event: any) => void) { + const eventStream = this.horizonServer + .effects() + .forAccount(config.contractId) + .cursor('now') + .stream({ + onmessage: (effect) => { + if (this.isShipmentEvent(effect, shipmentId)) { + callback(this.parseContractEvent(effect)); + } + }, + onerror: (error) => { + console.error('Event stream error:', error); + } + }); + + return eventStream; + } + + /** + * Listen for all contract events + */ + async listenForAllEvents(callback: (event: any) => void) { + const eventStream = this.horizonServer + .effects() + .forAccount(config.contractId) + .cursor('now') + .stream({ + onmessage: (effect) => { + if (effect.type === 'contract_credited' || effect.type === 'contract_debited') { + const event = this.parseContractEvent(effect); + if (event) { + callback(event); + } + } + }, + onerror: (error) => { + console.error('Event stream error:', error); + } + }); + + return eventStream; + } + + private isShipmentEvent(effect: any, shipmentId: number): boolean { + // Check if the event relates to the specific shipment + const event = this.parseContractEvent(effect); + return event && event.shipmentId === shipmentId; + } + + private parseContractEvent(effect: any): any { + try { + // Parse the contract event from Horizon effect + // This is a simplified example - actual parsing depends on event structure + const eventData = effect.data; + + return { + type: eventData.topic?.[0], + shipmentId: eventData.data?.[0], + timestamp: effect.created_at, + txHash: effect.transaction_hash, + ...eventData.data + }; + } catch (error) { + console.error('Failed to parse contract event:', error); + return null; + } + } +} +```` + +### Event Processing Service + +```typescript +// src/services/event-processor.service.ts +import { EventListenerService } from "./event-listener.service"; +import { ShipmentModel } from "../models/shipment.model"; + +export class EventProcessorService { + private eventListener: EventListenerService; + + constructor() { + this.eventListener = new EventListenerService(); + } + + async startProcessing() { + await this.eventListener.listenForAllEvents(async (event) => { + try { + await this.processEvent(event); + } catch (error) { + console.error("Failed to process event:", error); + } + }); + } + + private async processEvent(event: any) { + switch (event.type) { + case "shipment_created": + await this.handleShipmentCreated(event); + break; + case "status_updated": + await this.handleStatusUpdated(event); + break; + case "milestone_recorded": + await this.handleMilestoneRecorded(event); + break; + case "escrow_deposited": + await this.handleEscrowDeposited(event); + break; + case "escrow_released": + await this.handleEscrowReleased(event); + break; + default: + console.log("Unknown event type:", event.type); + } + } + + private async handleShipmentCreated(event: any) { + // Update MongoDB with new shipment + await ShipmentModel.create({ + shipmentId: event.shipmentId, + sender: event.sender, + receiver: event.receiver, + dataHash: event.dataHash, + status: "Created", + createdAt: new Date(event.timestamp), + txHash: event.txHash, + }); + + console.log(`Shipment ${event.shipmentId} created`); + } + + private async handleStatusUpdated(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + status: event.newStatus, + dataHash: event.dataHash, + updatedAt: new Date(event.timestamp), + lastTxHash: event.txHash, + }, + ); + + console.log( + `Shipment ${event.shipmentId} status updated to ${event.newStatus}`, + ); + } + + private async handleMilestoneRecorded(event: any) { + // Add milestone to shipment record + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + $push: { + milestones: { + checkpoint: event.checkpoint, + dataHash: event.dataHash, + timestamp: new Date(event.timestamp), + reporter: event.reporter, + txHash: event.txHash, + }, + }, + }, + ); + + console.log( + `Milestone ${event.checkpoint} recorded for shipment ${event.shipmentId}`, + ); + } + + private async handleEscrowDeposited(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + escrowAmount: event.amount, + escrowTxHash: event.txHash, + }, + ); + } + + private async handleEscrowReleased(event: any) { + await ShipmentModel.findOneAndUpdate( + { shipmentId: event.shipmentId }, + { + $inc: { escrowAmount: -event.amount }, + releaseTxHash: event.txHash, + }, + ); + } +} +``` + +## Transaction Verification + +### Verify Transaction Hash and Data Integrity + +```typescript +// src/services/verification.service.ts +import { StellarService } from "./stellar.service"; +import { ShipmentModel } from "../models/shipment.model"; +import { createHash } from "crypto"; + +export class VerificationService extends StellarService { + /** + * Verify transaction hash exists on-chain and compare data hash + */ + async verifyTransaction( + txHash: string, + expectedDataHash: string, + ): Promise<{ + valid: boolean; + onChain: boolean; + dataMatch: boolean; + details?: any; + }> { + try { + // 1. Get transaction from Stellar network + const transaction = await this.server.getTransaction(txHash); + + if (!transaction) { + return { + valid: false, + onChain: false, + dataMatch: false, + }; + } + + // 2. Extract data hash from transaction + const onChainDataHash = this.extractDataHashFromTransaction(transaction); + + // 3. Compare hashes + const dataMatch = onChainDataHash === expectedDataHash; + + return { + valid: transaction.successful && dataMatch, + onChain: true, + dataMatch, + details: { + ledger: transaction.ledger, + createdAt: transaction.created_at, + fee: transaction.fee_charged, + onChainDataHash, + expectedDataHash, + }, + }; + } catch (error) { + console.error("Transaction verification failed:", error); + return { + valid: false, + onChain: false, + dataMatch: false, + }; + } + } + + /** + * Verify shipment data integrity between MongoDB and blockchain + */ + async verifyShipmentIntegrity(shipmentId: number): Promise<{ + valid: boolean; + issues: string[]; + }> { + const issues: string[] = []; + + try { + // 1. Get shipment from MongoDB + const dbShipment = await ShipmentModel.findOne({ shipmentId }); + if (!dbShipment) { + issues.push("Shipment not found in database"); + return { valid: false, issues }; + } + + // 2. Get shipment from blockchain + const onChainShipment = await this.getShipmentFromChain(shipmentId); + if (!onChainShipment) { + issues.push("Shipment not found on blockchain"); + return { valid: false, issues }; + } + + // 3. Compare critical fields + if (dbShipment.sender !== onChainShipment.sender) { + issues.push("Sender mismatch"); + } + + if (dbShipment.receiver !== onChainShipment.receiver) { + issues.push("Receiver mismatch"); + } + + if (dbShipment.status !== onChainShipment.status) { + issues.push("Status mismatch"); + } + + // 4. Verify data hash + const computedHash = this.hashOffChainData(dbShipment.fullData); + if (computedHash !== onChainShipment.dataHash) { + issues.push("Data hash mismatch - data may be corrupted"); + } + + // 5. Verify transaction hashes + if (dbShipment.txHash) { + const txVerification = await this.verifyTransaction( + dbShipment.txHash, + dbShipment.dataHash, + ); + if (!txVerification.valid) { + issues.push("Creation transaction verification failed"); + } + } + + return { + valid: issues.length === 0, + issues, + }; + } catch (error) { + console.error("Integrity verification failed:", error); + return { + valid: false, + issues: ["Verification process failed"], + }; + } + } + + private async getShipmentFromChain(shipmentId: number) { + try { + const account = await this.getAccount(); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: config.networkPassphrase, + }) + .addOperation( + this.contract.call( + "get_shipment", + nativeToScVal(shipmentId, { type: "u64" }), + ), + ) + .setTimeout(30) + .build(); + + transaction.sign(this.sourceKeypair); + const response = await this.server.sendTransaction(transaction); + + if (response.status === "SUCCESS") { + return scValToNative(response.returnValue); + } + + return null; + } catch (error) { + console.error("Failed to get shipment from chain:", error); + return null; + } + } + + private extractDataHashFromTransaction(transaction: any): string { + // Extract data hash from transaction operations + // Implementation depends on transaction structure + try { + const operation = transaction.operations[0]; + // Parse the operation to extract data hash + return operation.parameters?.data_hash || ""; + } catch (error) { + console.error("Failed to extract data hash:", error); + return ""; + } + } + + private hashOffChainData(data: any): string { + const jsonString = JSON.stringify(data, Object.keys(data).sort()); + return createHash("sha256").update(jsonString).digest("hex"); + } +} +``` + +## Complete Examples + +### Express.js Route Implementation + +```typescript +// src/routes/shipments.ts +import { Router } from "express"; +import { ShipmentService } from "../services/shipment.service"; +import { VerificationService } from "../services/verification.service"; + +const router = Router(); +const shipmentService = new ShipmentService(); +const verificationService = new VerificationService(); + +// Create shipment +router.post("/shipments", async (req, res) => { + try { + const { + sender, + receiver, + carrier, + shipmentData, + paymentMilestones, + deadline, + } = req.body; + + const result = await shipmentService.createShipment({ + sender, + receiver, + carrier, + offChainData: shipmentData, + paymentMilestones, + deadline: new Date(deadline), + }); + + res.json({ + success: true, + shipmentId: result.shipmentId, + txHash: result.txHash, + dataHash: result.dataHash, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +// Update shipment status +router.put("/shipments/:id/status", async (req, res) => { + try { + const { id } = req.params; + const { caller, newStatus, updateData } = req.body; + + const result = await shipmentService.updateShipmentStatus( + caller, + parseInt(id), + newStatus, + updateData, + ); + + res.json(result); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +// Verify transaction +router.get("/verify/:txHash", async (req, res) => { + try { + const { txHash } = req.params; + const { expectedDataHash } = req.query; + + const verification = await verificationService.verifyTransaction( + txHash, + expectedDataHash as string, + ); + + res.json(verification); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +export default router; +``` + +### MongoDB Schema + +```typescript +// src/models/shipment.model.ts +import mongoose from "mongoose"; + +const milestoneSchema = new mongoose.Schema({ + checkpoint: String, + dataHash: String, + timestamp: Date, + reporter: String, + txHash: String, + gpsCoordinates: { + latitude: Number, + longitude: Number, + }, + sensorData: { + temperature: Number, + humidity: Number, + pressure: Number, + }, +}); + +const shipmentSchema = new mongoose.Schema({ + shipmentId: { type: Number, unique: true, required: true }, + sender: { type: String, required: true }, + receiver: { type: String, required: true }, + carrier: { type: String, required: true }, + status: { type: String, required: true }, + dataHash: { type: String, required: true }, + txHash: String, + createdAt: Date, + updatedAt: Date, + deadline: Date, + escrowAmount: Number, + milestones: [milestoneSchema], + fullData: { + description: String, + weight: Number, + dimensions: { + length: Number, + width: Number, + height: Number, + }, + specialInstructions: String, + photos: [String], + documents: [String], + }, +}); + +export const ShipmentModel = mongoose.model("Shipment", shipmentSchema); +``` + +### Environment Variables + +```bash +# .env +NODE_ENV=development +STELLAR_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +SHIPMENT_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +TOKEN_CONTRACT_ID=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +MONGODB_URI=mongodb://localhost:27017/navin +``` + +## Best Practices + +1. **Error Handling**: Always wrap Stellar operations in try-catch blocks +2. **Rate Limiting**: Implement rate limiting for contract calls to avoid hitting network limits +3. **Data Validation**: Validate all input data before creating hashes or submitting transactions +4. **Event Deduplication**: Handle duplicate events that may occur during network issues +5. **Transaction Fees**: Monitor and adjust transaction fees based on network conditions +6. **Security**: Never expose private keys in client-side code or logs + +## Testing + +```typescript +// src/tests/shipment.test.ts +import { ShipmentService } from "../services/shipment.service"; +import { VerificationService } from "../services/verification.service"; + +describe("Shipment Integration", () => { + let shipmentService: ShipmentService; + let verificationService: VerificationService; + + beforeEach(() => { + shipmentService = new ShipmentService(); + verificationService = new VerificationService(); + }); + + it("should create shipment and verify transaction", async () => { + const shipmentData = { + sender: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + receiver: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + carrier: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + offChainData: { + description: "Test shipment", + weight: 10.5, + specialInstructions: "Handle with care", + }, + paymentMilestones: [ + { checkpoint: "pickup", percentage: 30 }, + { checkpoint: "delivery", percentage: 70 }, + ], + deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }; + + const result = await shipmentService.createShipment(shipmentData); + expect(result.success).toBe(true); + expect(result.shipmentId).toBeGreaterThan(0); + expect(result.txHash).toBeDefined(); + + // Verify the transaction + const verification = await verificationService.verifyTransaction( + result.txHash, + result.dataHash, + ); + expect(verification.valid).toBe(true); + expect(verification.onChain).toBe(true); + expect(verification.dataMatch).toBe(true); + }); +}); +``` + +This integration guide provides complete TypeScript examples for interacting with the Navin shipment contract, including contract invocation, event listening, and transaction verification patterns that your Express backend can use. diff --git a/package-lock.json b/package-lock.json index c71befb..c00cd17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "navin-contracts", + "name": "Anchor-Contract", "lockfileVersion": 3, "requires": true, "packages": {} From bb30b9c2fe09b27807490b43a97acc3099d6ea00 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sun, 14 Jun 2026 07:50:14 +0000 Subject: [PATCH 02/15] Fixes completed --- .cargo/config.toml | 2 ++ .gitignore | 3 +++ CONTRIBUTING.md | 4 ++-- Cargo.lock | 3 +++ Makefile | 2 ++ scripts/build.sh | 3 +++ scripts/check_wasm_size.sh | 3 +++ 7 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 456195e..c438f79 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,6 @@ [target.wasm32-unknown-unknown] rustflags = [ "-C", "target-feature=-reference-types,-multivalue,-bulk-memory", + + ] diff --git a/.gitignore b/.gitignore index ec453f1..e283d57 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,8 @@ issue.md skills-lock.json PR_DESCRIPTION.md + + + # Testnet deployment configuration .env.testnet diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e756501..5b237cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Navin +# Contributing to ANCHOR-FLOW Thank you for your interest in contributing to Navin! We truly appreciate it!! This guide will help you get started and ensure your contributions can be smoothly integrated. @@ -29,7 +29,7 @@ Before contributing, ensure you have the following installed: 2. Clone your fork locally: ```bash - git clone https://github.com/YOUR-USERNAME/navin-contracts.git + git clone https://github.com/YOUR-USERNAME/ANCHOR-FLOW-contracts.git cd navin-contracts ``` diff --git a/Cargo.lock b/Cargo.lock index 83d5dd5..296aedc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,9 @@ version = 4 [[package]] + + + name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Makefile b/Makefile index d129b1c..2caafb1 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ # Default target help: + + @echo "Navin Smart Contracts - Available Commands" @echo "" @echo " make generate-schema-shipment - Generate shipment contract ABI schema" diff --git a/scripts/build.sh b/scripts/build.sh index d18d965..cc06273 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,6 +6,9 @@ echo "Building Soroban contracts..." # Build both contracts stellar contract build + + + # Verify WASM files exist WASM_DIR="target/wasm32-unknown-unknown/release" TOKEN_WASM="$WASM_DIR/navin_token.wasm" diff --git a/scripts/check_wasm_size.sh b/scripts/check_wasm_size.sh index 8dd4eba..6ca4c6d 100755 --- a/scripts/check_wasm_size.sh +++ b/scripts/check_wasm_size.sh @@ -7,6 +7,9 @@ BUDGETS=( "shipment.wasm:200" ) + + + WASM_DIR="target/wasm32-unknown-unknown/release" EXIT_CODE=0 From c61a45a437d44bb89b3784e787d774d19ac7f77d Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sun, 14 Jun 2026 07:52:18 +0000 Subject: [PATCH 03/15] logs --- SECURITY.md | 3 +++ build.log | 2 ++ cargo.log | 2 ++ 3 files changed, 7 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index b5d6936..2228a7d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,9 @@ The Navin team takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings. + + + ### Please DO NOT: - Open a public GitHub issue for security vulnerabilities diff --git a/build.log b/build.log index dbd60ab..a0c56fa 100644 --- a/build.log +++ b/build.log @@ -2,6 +2,8 @@ Compiling soroban-env-common v22.1.3 Compiling soroban-sdk-macros v22.0.7 Compiling soroban-sdk v22.0.7 + + Checking soroban-env-host v22.1.3 Checking soroban-ledger-snapshot v22.0.7 Checking shipment v0.0.0 (/home/izk/Documents/DRIPS/navin-contracts/contracts/shipment) diff --git a/cargo.log b/cargo.log index fcf5d1b..57bd284 100644 --- a/cargo.log +++ b/cargo.log @@ -12,6 +12,8 @@ warning: unused import: `crate::test_utils::*` 3 | use crate::test_utils::*; | ^^^^^^^^^^^^^^^^^^^^ + + warning: use of deprecated method `soroban_sdk::Env::register_stellar_asset_contract`: use [Env::register_stellar_asset_contract_v2] --> contracts/shipment/src/test_token_compatibility.rs:39:17 | From 244659d81cc6411c6c6de9677094cfab6d49971e Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sun, 14 Jun 2026 07:53:09 +0000 Subject: [PATCH 04/15] cargo toml --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a04ea4f..c8cde47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ members = [ [workspace.dependencies] soroban-sdk = "22.0.0" + + + [profile.release] opt-level = "z" overflow-checks = true From 2f505cb996da3b51596c82a1618bd5feceec1ea9 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:49:53 +0000 Subject: [PATCH 05/15] client --- docs/BUDGET_REPORT.md | 4 ++++ docs/client-examples.md | 2 ++ docs/deployment.md | 3 +++ 3 files changed, 9 insertions(+) diff --git a/docs/BUDGET_REPORT.md b/docs/BUDGET_REPORT.md index 5ac1e11..2276f70 100644 --- a/docs/BUDGET_REPORT.md +++ b/docs/BUDGET_REPORT.md @@ -190,6 +190,10 @@ When comparing a new run against this baseline: - New `env.events().publish()` calls (each costs ~10 k instructions) - Increased `BytesN` or `Symbol` allocations + + + + ### Common causes of memory regressions - Cloning large `Vec` or `Map` values instead of borrowing diff --git a/docs/client-examples.md b/docs/client-examples.md index 9f35a4c..9369464 100644 --- a/docs/client-examples.md +++ b/docs/client-examples.md @@ -4,6 +4,8 @@ Short, practical snippets for common shipment contract calls. Copy the call shap ## Table of Contents + + 1. [Setup](#setup) 2. [Initialization](#initialization) 3. [Shipment Management](#shipment-management) diff --git a/docs/deployment.md b/docs/deployment.md index 2b308ee..fd03da1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -6,6 +6,9 @@ This guide walks you through deploying the Navin shipment tracking contracts to Before deploying, ensure you have: + + + 1. **Rust toolchain** installed with the `wasm32-unknown-unknown` target: ```bash From 5e4270c6c7df1743bf88f946d3e19875e889bf0f Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:51:20 +0000 Subject: [PATCH 06/15] events --- docs/events.md | 4 ++++ docs/integration-guide.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/docs/events.md b/docs/events.md index ff82930..db5372a 100644 --- a/docs/events.md +++ b/docs/events.md @@ -8,6 +8,10 @@ --- + + + + ## Table of Contents 1. [Event Format](#event-format) diff --git a/docs/integration-guide.md b/docs/integration-guide.md index c9d1755..33f72fc 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -7,6 +7,8 @@ Complete guide for integrating the Navin shipment tracking smart contract with y Complete guide for integrating the Navin shipment tracking smart contract with your Express.js backend using the Stellar JavaScript/TypeScript SDK. + + ## Table of Contents 1. [Architecture Overview](#architecture-overview) From 9d788f00c7a5a2b136a7f0679d375a06dee451c5 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:51:53 +0000 Subject: [PATCH 07/15] stimution --- docs/simulation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/simulation.md b/docs/simulation.md index 6fdb08f..e85da5f 100644 --- a/docs/simulation.md +++ b/docs/simulation.md @@ -8,6 +8,8 @@ no fees are charged during simulation. ## Why simulate? + + | Concern | What simulation tells you | |---|---| | Will the call succeed? | Returns the same error the live call would return | @@ -19,6 +21,10 @@ no fees are charged during simulation. ## Prerequisites + + + + 1. **Stellar CLI** installed ``` cargo install --locked stellar-cli --features opt From 0b55d48d50e8a2c3156b3d6561b4c461a027ca84 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:52:18 +0000 Subject: [PATCH 08/15] policy --- docs/storage-key-registry.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/storage-key-registry.md b/docs/storage-key-registry.md index 2555a88..d02a6be 100644 --- a/docs/storage-key-registry.md +++ b/docs/storage-key-registry.md @@ -10,6 +10,9 @@ This document defines a migration-safe registry for `DataKey` usage in `contract ## Policy + + + - Do not reorder or repurpose existing `DataKey` variants. - New variants are append-only and must include a clear doc comment. - Removed keys are marked as deprecated in docs; on-chain discriminants remain reserved. From 1ff1ebc2d4d72061ae937f3c0b3246ccb669a951 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:45:58 +0000 Subject: [PATCH 09/15] contributing --- CONTRIBUTING.md | 427 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b237cc..dc46e3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -425,3 +425,430 @@ git rebase main Thank you for contributing to Navin! Together, we're building a transparent and secure delivery tracking platform. +# Contributing to ANCHOR-FLOW + +Thank you for your interest in contributing to Navin! We truly appreciate it!! +This guide will help you get started and ensure your contributions can be smoothly integrated. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Making Contributions](#making-contributions) +- [Pre-PR Checklist](#pre-pr-checklist) +- [CI/CD Requirements](#cicd-requirements) +- [Code Standards](#code-standards) +- [Testing Guidelines](#testing-guidelines) +- [Getting Help](#getting-help) + +## Getting Started + +### Prerequisites + +Before contributing, ensure you have the following installed: + +**Rust** (latest stable version) + +### Fork and Clone + +1. Fork the repository on GitHub by clicking the "Fork" button + +2. Clone your fork locally: + + ```bash + git clone https://github.com/YOUR-USERNAME/ANCHOR-FLOW-contracts.git + cd navin-contracts + ``` + +3. Add the upstream repository: (So you can pull and sync new updates) + NB: This is optional as you can just sync fork through github website + ```bash + git remote add upstream https://github.com/Navin-xmr/navin-contracts.git + ``` + +## Development Setup + +### Verify Your Environment + +Run these commands to ensure everything is set up correctly: + +```bash +# Check Rust version +rustc --version + +# Check cargo +cargo --version + +``` + +## Making Contributions + +### Step 1: Create a Branch + +Always create a new branch for your work. Never commit directly to `main` or `develop`. + +```bash +# Update your local main branch +git checkout main + +``` + +If you added the upstream branch, pull the changes + +(optional - you can just use github to sync your fork) + +``` +git pull upstream main +``` + +## Create a new branch with a descriptive name + +git checkout -b issue# + +``` +### Examples of good branch names: + - issue#23 + - issue#45 +``` + +### Step 2: Make Your Changes + +1. Write your code following our [Code Standards](#code-standards) +2. Add or update tests for your changes +3. Update documentation if needed +4. Keep commits focused and precise + +### Step 3: Test Your Changes Locally + +Before pushing, **always run these commands locally** to ensure CI will pass: + +```bash +# 1. Format your code +cargo fmt + +# 2. Run clippy (linter) - must pass with no warnings +cargo clippy --all-targets --all-features -- -D warnings + +# 3. Run all tests +cargo test + +# 4. Build contracts (WASM target) +cargo build --target wasm32-unknown-unknown --release +``` + +**Important**: If any of these commands fail, fix the issues before pushing! + +### Step 4: Commit Your Changes + +```bash +# Stage your changes +git add . + +# Commit with a descriptive message +git commit -m "type: brief description + +Longer explanation if needed. + +- Detail 1 +- Detail 2" +``` + +**Commit Message Format we prefer:** + +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation changes +- `test:` - Adding or updating tests +- `refactor:` - Code refactoring +- `chore:` - Maintenance tasks + +**Examples:** + +```bash +git commit -m "feat: add delivery status tracking" +git commit -m "fix: resolve timestamp overflow in lock_assets" +git commit -m "docs: update installation instructions" +``` + +### Step 5: Push to Your Fork + +```bash +git push origin issue# +``` + +### Step 6: Create a Pull Request + +1. Go to your fork on GitHub +2. Click "Compare & pull request" +3. Fill out the PR template: + - **Title**: Clear, concise description + - **Description**: Explain what and why + - **Testing**: Describe how you tested + - **Checklist**: Complete all items + +## Pre-PR Checklist + +Before submitting your PR, ensure you've completed ALL of these steps: + +- [ ] Code is formatted with `cargo fmt` +- [ ] No clippy warnings: `cargo clippy --all-targets --all-features -- -D warnings` +- [ ] All tests pass: `cargo test` +- [ ] WASM builds successfully: `cargo build --target wasm32-unknown-unknown --release` +- [ ] New tests added for new functionality +- [ ] Old tests previously passing before you added changes still pass +- [ ] Documentation updated (if applicable) +- [ ] Branch is up to date with `main` or `dev` + +## CI/CD Requirements + +Our CI pipeline runs the following checks. **All must pass** before your PR can be merged: + +### 1. Code Formatting Check + +```bash +# Command run by CI: +cargo fmt --check + +# To fix formatting issues: +cargo fmt +``` + +### 2. Clippy Lints + +```bash +# Command run by CI: +cargo clippy --all-targets --all-features -- -D warnings + +# This fails on ANY warnings, so fix all clippy suggestions +``` + +### 3. Tests + +```bash +# Command run by CI: +cargo test + +# Make sure all tests pass locally +``` + +### 4. WASM Build + +```bash +# Command run by CI: +cargo build --target wasm32-unknown-unknown --release + +# Ensure contracts compile to WASM without errors +``` + +### 5. Security Audit + +```bash +# Command run by CI: +cargo audit + +# Checks for known security vulnerabilities in dependencies +``` + +### 6. WASM Size Budget + +To ensure our contracts remain deployable on-chain, we enforce strict size limits on the generated WASM files. + +```bash +# Command run by CI: +./scripts/check_wasm_size.sh +``` + +**Budget Update Policy:** +- **Thresholds**: Currently 192KB for `shipment` and 25KB for `token`. +- **Increases**: Budget increases must be justified in the PR description (e.g., due to critical new features) and approved by a maintainer. +- **Optimization**: Always attempt to optimize the code or reduce dependencies before requesting a budget increase. + +## Code Standards + +### Rust Style Guide + +- Follow the official [Rust Style Guide](https://doc.rust-lang.org/nightly/style-guide/) +- Use `cargo fmt` to automatically format code +- Use descriptive variable names relevant to the feature + +### Contract-Specific Guidelines + +1. **Error Handling** + - Always use proper error types (don't use `panic!`) + - Return `Result` for fallible operations + + ```rust + // Good + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), Error> { + if amount <= 0 { + return Err(Error::InvalidAmount); + } + Ok(()) + } + + // Bad + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + assert!(amount > 0); // Don't use assertions for validation + } + ``` + +2. **Authentication** + - Always verify addresses with `require_auth()` + + ```rust + pub fn withdraw(env: Env, from: Address, amount: i128) -> Result<(), Error> { + from.require_auth(); // Always first! + // ... rest of function + } + ``` + +3. **Storage** + - Use type-safe storage keys + - Document storage layout + - Consider gas costs + +4. **Documentation** + - Add doc comments to all public functions + - Explain complex logic + - Document error conditions + + ```rust + /// Deposits assets into the vault + /// + /// # Arguments + /// * `from` - The address depositing assets + /// * `amount` - Amount to deposit (must be positive) + /// + /// # Errors + /// Returns `InvalidAmount` if amount <= 0 + pub fn deposit(env: Env, from: Address, amount: i128) -> Result<(), Error> { + // implementation + } + ``` + +## Testing Guidelines + +### Writing Tests + +1. **Test Structure** + + ```rust + #[test] + fn test_descriptive_name() { + // Setup + let env = Env::default(); + let contract = setup_contract(&env); + + // Execute + let result = contract.function_to_test(); + + // Assert + assert!(result.is_ok()); + } + ``` + +2. **Test Coverage** + - Test error conditions + - Test edge cases (zero, negative, maximum values) + - Test access control (for scenarios like admin or regular users) + +3. **Test Organization** + - Keep tests in `test.rs` or dedicated test modules + - Group related tests together + - Use descriptive test names + +### Example Test + +```rust +#[test] +fn test_withdraw_insufficient_funds() { + let env = Env::default(); + let contract_id = env.register_contract(None, NavinShipment); + let client = NavinShipmentClient::new(&env, &contract_id); + + let user = Address::generate(&env); + + // Try to withdraw more than deposited + let result = client.try_withdraw(&user, &user, &1000); + + // Should fail with InsufficientFunds + assert!(result.is_err()); +} +``` + +## Common Issues and Solutions + +### Issue: Clippy warnings about needless borrow + +```rust +// Wrong +Vec::new(&env) + +// Correct +Vec::new(env) +``` + +### Issue: Assertion on constants + +```rust +// Wrong +assert!(true, "Always passes"); + +// Correct - Remove unnecessary assertions +// Just call the function or use proper assertions +``` + +### Issue: CI fails but local tests pass + +1. Ensure you've run ALL pre-PR checklist items +2. Pull latest changes from upstream +3. Clear cargo cache and rebuild: + ```bash + cargo clean + cargo test + ``` + +## Getting Help + +- **Bugs**: Open an [Issue](https://github.com/Navin-xmr/navin-contracts/issues) +- **Security**: Email navinxmr@gmail.com +- **General**: Join our Telegram - [Telegram Group Chat](https://t.me/+3svwFsQME6k1YjI0) + +## Review Process + +1. **Automated Checks**: CI must pass (typically 5 minutes) +2. **Code Review**: Maintainer reviews code (1-3 days) +3. **Revisions**: Address feedback. fix conflicts and push updates + +### Disclaimer: + +Please do not just combine both conflicts when trying to resolve merge conflicts then just push. Such PRs will not be merged. Actually check the differences, add and fix issues, ensure your build and tests work then you can push. + +## Quick Command Reference + +```bash +# Complete pre-submission checklist +cargo fmt +cargo clippy --all-targets --all-features -- -D warnings +cargo test +cargo build --target wasm32-unknown-unknown --release + +# Create and switch to a new branch +git checkout -b issue# + +# Stage, commit, and push +git add . +git commit -m "feat: your change description" +git push origin issue# + +# Update your branch with latest upstream changes (Optional - Can be done on Website) +git checkout main +git pull upstream main +git checkout issue# +git rebase main +``` + +--- + +Thank you for contributing to Navin! +Together, we're building a transparent and secure delivery tracking platform. From 8d19b60501f660a7e0be18da1d2cd5aa35a5d35c Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:46:40 +0000 Subject: [PATCH 10/15] toml --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8cde47..3c97799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,6 @@ members = [ soroban-sdk = "22.0.0" - - [profile.release] opt-level = "z" overflow-checks = true From b6ed78f118bf72400002cb7476ad987389dbd858 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:47:05 +0000 Subject: [PATCH 11/15] tomls --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 3c97799..6308772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "contracts/*", ] + + [workspace.dependencies] soroban-sdk = "22.0.0" From a41c20957a6241621306be2b7bb7cbac5759601b Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:35:51 +0100 Subject: [PATCH 12/15] Update BUDGET_REPORT.md --- docs/BUDGET_REPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/BUDGET_REPORT.md b/docs/BUDGET_REPORT.md index 2276f70..d1d6135 100644 --- a/docs/BUDGET_REPORT.md +++ b/docs/BUDGET_REPORT.md @@ -1,4 +1,4 @@ -# Soroban Budget Baseline Report +# Soroban Budget Baseline Reports Contract: **shipment** SDK version: **soroban-sdk 22.0.0** From 4a7e162747ec226fb51f5f99a4c2bc5e73d425e7 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:38:29 +0100 Subject: [PATCH 13/15] Update deployment.md --- docs/deployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deployment.md b/docs/deployment.md index fd03da1..b9b702b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -9,7 +9,7 @@ Before deploying, ensure you have: -1. **Rust toolchain** installed with the `wasm32-unknown-unknown` target: +1. **Rust toolchain** installed with the `wasm32-unknown-unknown` targets: ```bash rustup target add wasm32-unknown-unknown From 429927f5f18c2ca390b5aaa3529f4542f5bd80d3 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:38:52 +0100 Subject: [PATCH 14/15] Fix typo in architecture note heading --- docs/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/events.md b/docs/events.md index db5372a..df04804 100644 --- a/docs/events.md +++ b/docs/events.md @@ -1,6 +1,6 @@ # Shipment Contract — Event Schema Reference -> **Architecture note:** Navin uses a **Hash-and-Emit** pattern. Heavy payloads (GPS traces, +> **Architectures note:** Navin uses a **Hash-and-Emit** pattern. Heavy payloads (GPS traces, > sensor readings, dispute evidence, milestone metadata) are stored **off-chain**. The contract > emits only the `shipment_id`, relevant identifiers, and a `BytesN<32>` SHA-256 hash of the > full payload. The backend indexer verifies integrity by re-hashing the stored payload and From af35ae4d01572f333eccfc636bd9d1bff678deb9 Mon Sep 17 00:00:00 2001 From: Nanafancy <133337507+Nanafancy@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:40:55 +0100 Subject: [PATCH 15/15] Update events.md --- docs/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/events.md b/docs/events.md index df04804..4d57692 100644 --- a/docs/events.md +++ b/docs/events.md @@ -236,7 +236,7 @@ Emitted when a carrier reports a transit checkpoint. The full milestone payload ### `delivery_success` -Emitted when the receiver confirms that a shipment has been successfully delivered. Used by the analytics pipeline to compute carrier punctuality metrics. +Emitteds when the receiver confirms that the shipment has been successfully delivered. Used by the analytics pipeline to compute carrier punctuality metrics. **Topic:** `"delivery_success"` **Emitted by:** `confirm_delivery`