Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/oracle-submission-retention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Oracle submission history

`POST /oracle/submissions` registers a submitted Stellar transaction for verification.
It requires `oracle:submit`; `GET /oracle/submissions` requires `oracle:verify` and
supports `limit` (1–100) and `offset`. Registration means pending, not trusted.

The database keeps payload hash, submitter, transaction hash, verification status,
failure reason and timestamps. Unique indexes reject duplicate payload hashes and
transaction hashes atomically, including concurrent submissions. Hashes are
normalized to lowercase before lookup. Pending verification reads at most 100 rows.

Retention policy: retain this compact register indefinitely, including rejected
records. No automatic deletion or TTL is applied. Back up it with the application
database. Any future archival implementation must leave both unique replay keys
online and preserve the verification audit data in a recoverable archive; deleting
keys would allow historical replays. This conservative policy needs no retention
worker and applies across process restarts.
36 changes: 36 additions & 0 deletions docs/stellar-oracle-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Stellar oracle verification

Configure `SOROBAN_RPC_URL` (HTTPS), `ORACLE_CONTRACT_ADDRESS` (Stellar C address)
and `STELLAR_NETWORK_PASSPHRASE` (defaults to Stellar testnet). For mainnet use
the mainnet RPC endpoint and `Public Global Stellar Network ; September 2015`.
`ORACLE_MIN_CONFIRMATIONS` defaults to 1 closed ledger, `ORACLE_RPC_MAX_RETRIES`
to 3 and `ORACLE_RPC_RETRY_DELAY_MS` to 250. Transient transport failures use
bounded exponential backoff; failed transactions and invalid invocations do not.

The upstream repository formerly called Trellis is now
[Trellis-contracts](https://github.com/TRELLIS-STELLAR/Trellis-contracts).
The verifier matches its
[oracle contract](https://github.com/TRELLIS-STELLAR/Trellis-contracts/blob/main/contracts/oracle-contract/src/lib.rs)
`submit_price(submitter: Address, feed_id: Symbol, price: i128, decimals: u32,
timestamp: u64, nonce: u64)` interface. It accepts a single direct invocation.
The successful transaction is evidence that the contract's submitter authorization,
nonce and price validation ran. The returned envelope hash, network, contract,
method, argument types, submitter and payload digest must all match.

After submitting a Stellar transaction, register its reference using
`POST /oracle/submissions`. The payload hash is lowercase SHA-256 of the canonical
XDR `ScVal` vector containing all six `submit_price` arguments in ABI order.
`hashOracleArguments` implements this shared representation; use Stellar SDK
typed values (Address, Symbol, i128, u32, u64, u64) when constructing it.
This binds the price, precision, observation time and nonce to the submitter.
The verifier polls pending references in batches of 100 and persists verified or
rejected outcomes. Missing transactions and insufficient confirmations stay pending.

Soroban RPC retains a limited transaction window. Old references require a compatible
archival RPC; missing history must never be considered proof of verification.

The separate legacy payload signing and submission services use a different protocol.
Their payloads are not automatically treated as Stellar submissions. This verifier
requires a successful Stellar `submit_price` transaction and its matching XDR digest;
it does not invent a contract ABI for legacy payloads. Changes to the upstream ABI
must update this document and the invocation fixtures together.
26 changes: 26 additions & 0 deletions docs/user-api-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# User API keys

With an authenticated account JWT, use `POST /auth/api-keys` with `name`,
`permissions` (defaults to `["read"]`) and optional `expiresInDays` (1–365).
The response includes the random key exactly once. Store it securely; it cannot
be retrieved. The database stores only a SHA-256 digest of its 256-bit random
value. `GET /auth/api-keys` returns metadata only, and `DELETE /auth/api-keys/:id`
revokes only the caller's own key. Rotate by creating a replacement, switching
the consumer, then revoking the previous key. API keys cannot manage other keys.

Send `X-API-Key` for programmatic requests. Scopes restrict the account's existing
permissions; they never replace ownership or role checks. On routes with explicit
permission metadata every required permission must appear in the key's scopes.
Other routes require `read` for GET/HEAD/OPTIONS and `write` for other methods.
Supported scopes are `read`, `write` and the application's named permissions.

The strategy caches a maximum of 1,000 metadata entries for 30 seconds. A conditional
database update checks revocation and expiry and records last use on every successful
authentication. Consequently revocation applies immediately across all API instances,
even when another instance cached metadata or a consumer holds an API-key-issued JWT.
Authentication fails closed if the database is unavailable.

`SYSTEM_API_KEYS` remains supported for migration and logs a deprecation warning.
Move consumers to persisted keys and remove static configuration when migrated.
Old API-key JWTs without a tracked key identity must reauthenticate. No static key
removal date is imposed by this change.
8 changes: 8 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { EnvironmentVariables } from "./config/env.validation";

import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { TargetAllocationVersion } from "./portfolio/entities/target-allocation.entity";
import { SubmissionHistory } from "./blockchain/oracle/entities/submission-history.entity";
import { ApiKey } from "./core/auth/entities/api-key.entity";
import { PortfolioModule as TargetAllocationModule } from "./portfolio/portfolio.module";

// Modules – core
import { AuthModule } from "./core/auth/auth.module";
Expand Down Expand Up @@ -247,10 +251,12 @@ import { InvariantReportEntity } from "./monitoring/invariant-monitor/entities/i
database: "swaptrade",
entities: [
User,
ApiKey,
EmailVerification,
Wallet,
SignedPayload,
SubmissionNonce,
SubmissionHistory,
PriceRecord,
AgentEvent,
ComputeResult,
Expand All @@ -259,6 +265,7 @@ import { InvariantReportEntity } from "./monitoring/invariant-monitor/entities/i
SensitiveActionEvent,
SensitiveActionChainHead,
Portfolio,
TargetAllocationVersion,
PortfolioAsset,
Transaction,
RiskProfile,
Expand Down Expand Up @@ -326,6 +333,7 @@ import { InvariantReportEntity } from "./monitoring/invariant-monitor/entities/i
},
}),

TargetAllocationModule,
EventEmitterModule.forRoot(),

AuthModule,
Expand Down
38 changes: 38 additions & 0 deletions src/blockchain/oracle/entities/submission-history.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";

@Entity("oracle_submission_history")
@Index("UQ_oracle_history_payload", ["payloadHash"], { unique: true })
@Index("UQ_oracle_history_transaction", ["transactionHash"], { unique: true })
@Index("IDX_oracle_history_pending", ["verificationStatus", "updatedAt"])
export class SubmissionHistory {
@PrimaryGeneratedColumn("uuid")
id: string;

@Column({ type: "varchar", length: 64 })
payloadHash: string;

@Column({ type: "varchar", length: 56 })
submitter: string;

@Column({ type: "varchar", length: 64 })
transactionHash: string;

@Column({ type: "varchar", length: 16, default: "pending" })
verificationStatus: "pending" | "verified" | "rejected";

@Column({ type: "text", nullable: true })
verificationError: string | null;

@CreateDateColumn({ type: "timestamp" })
createdAt: Date;

@UpdateDateColumn({ type: "timestamp" })
updatedAt: Date;
}
20 changes: 18 additions & 2 deletions src/blockchain/oracle/oracle.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,34 @@ import { SignedPayload } from "./entities/signed-payload.entity";
import { SubmissionNonce } from "./entities/submission-nonce.entity";
import { PriceRecord } from "./entities/price-record.entity";
import { AuditModule } from "src/infrastructure/audit/audit.module";
import { SubmissionHistory } from "./entities/submission-history.entity";
import { SubmissionHistoryService } from "./services/submission-history.service";
import { SubmissionHistoryController } from "./submission-history.controller";
import { StellarOracleAdapter } from "./services/stellar-oracle.adapter";

/**
* Oracle Module
* Provides services for signing and submitting verified payloads on-chain
*/
@Module({
imports: [
TypeOrmModule.forFeature([SignedPayload, SubmissionNonce, PriceRecord]),
TypeOrmModule.forFeature([
SignedPayload,
SubmissionNonce,
PriceRecord,
SubmissionHistory,
]),
ConfigModule,
AuditModule,
],
controllers: [OracleController, PriceFeedController],
controllers: [
OracleController,
PriceFeedController,
SubmissionHistoryController,
],
providers: [
StellarOracleAdapter,
SubmissionHistoryService,
OracleService,
PayloadSigningService,
NonceManagementService,
Expand All @@ -36,6 +51,7 @@ import { AuditModule } from "src/infrastructure/audit/audit.module";
PriceFeedService,
],
exports: [
SubmissionHistoryService,
OracleService,
PayloadSigningService,
NonceManagementService,
Expand Down
Loading
Loading