Skip to content

Commit 9d45d5f

Browse files
Merge pull request #89 from A6dulmalik/feature/68-cross-chain-price-oracle
feat: cross-chain price oracle integration
2 parents c473bd5 + 7f5abf5 commit 9d45d5f

9 files changed

Lines changed: 713 additions & 2 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ LLAMA_API_BASE_URL=http://localhost:8000
1717

1818
# Blockchain
1919
ETH_RPC_URL=https://mainnet.infura.io/v3/your-project-id
20+
BSC_RPC_URL=https://bsc-dataseed.binance.org
2021
CHAIN_ID=1
2122

2223
# Redis (for caching and WebSocket)

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { Wallet } from "./core/auth/entities/wallet.entity";
5454
// Oracle entities
5555
import { SignedPayload } from "./blockchain/oracle/entities/signed-payload.entity";
5656
import { SubmissionNonce } from "./blockchain/oracle/entities/submission-nonce.entity";
57+
import { PriceRecord } from "./blockchain/oracle/entities/price-record.entity";
5758

5859
// Audit entities
5960
import { AgentEvent } from "./infrastructure/audit/entities/agent-event.entity";
@@ -148,6 +149,7 @@ import { ProfilingMiddleware } from "./profiling/profiling.middleware";
148149
Wallet,
149150
SignedPayload,
150151
SubmissionNonce,
152+
PriceRecord,
151153
AgentEvent,
152154
ComputeResult,
153155
ProvenanceRecord,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
2+
import { IsEnum, IsString, IsOptional, IsInt, Min, Max } from "class-validator";
3+
import { SupportedChain, PriceSource } from "../entities/price-record.entity";
4+
5+
export class GetPriceDto {
6+
@ApiProperty({ example: "ETH", description: "Asset symbol" })
7+
@IsString()
8+
asset: string;
9+
10+
@ApiProperty({ enum: SupportedChain, example: SupportedChain.ETHEREUM })
11+
@IsEnum(SupportedChain)
12+
chain: SupportedChain;
13+
}
14+
15+
export class GetHistoricalPricesDto {
16+
@ApiProperty({ example: "ETH" })
17+
@IsString()
18+
asset: string;
19+
20+
@ApiProperty({ enum: SupportedChain })
21+
@IsEnum(SupportedChain)
22+
chain: SupportedChain;
23+
24+
@ApiPropertyOptional({ example: 100, default: 100, minimum: 1, maximum: 1000 })
25+
@IsOptional()
26+
@IsInt()
27+
@Min(1)
28+
@Max(1000)
29+
limit?: number = 100;
30+
}
31+
32+
export class SourcePriceDto {
33+
@ApiPropertyOptional({ example: 2000.5 })
34+
chainlink?: number;
35+
36+
@ApiPropertyOptional({ example: 2001.0 })
37+
band?: number;
38+
39+
@ApiPropertyOptional({ example: 1999.8 })
40+
uniswap_twap?: number;
41+
}
42+
43+
export class PriceResponseDto {
44+
@ApiProperty({ example: "ETH" })
45+
asset: string;
46+
47+
@ApiProperty({ enum: SupportedChain })
48+
chain: SupportedChain;
49+
50+
@ApiProperty({ example: 2000.43 })
51+
price: number;
52+
53+
@ApiProperty({ type: SourcePriceDto })
54+
sourcePrices: Record<PriceSource, number>;
55+
56+
@ApiProperty({ example: false })
57+
deviationAlert: boolean;
58+
59+
@ApiProperty({ example: 0.0612 })
60+
maxDeviationPercent: number;
61+
62+
@ApiProperty()
63+
timestamp: Date;
64+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
Index,
7+
} from "typeorm";
8+
9+
export enum SupportedChain {
10+
ETHEREUM = "ethereum",
11+
BSC = "bsc",
12+
POLYGON = "polygon",
13+
ARBITRUM = "arbitrum",
14+
OPTIMISM = "optimism",
15+
AVALANCHE = "avalanche",
16+
}
17+
18+
export enum PriceSource {
19+
CHAINLINK = "chainlink",
20+
BAND = "band",
21+
UNISWAP_TWAP = "uniswap_twap",
22+
}
23+
24+
@Entity("price_records")
25+
@Index(["asset", "chain", "createdAt"])
26+
@Index(["asset", "createdAt"])
27+
export class PriceRecord {
28+
@PrimaryGeneratedColumn("uuid")
29+
id: string;
30+
31+
/** Asset symbol, e.g. "ETH", "BTC" */
32+
@Column({ type: "varchar", length: 20 })
33+
asset: string;
34+
35+
@Column({ type: "enum", enum: SupportedChain })
36+
chain: SupportedChain;
37+
38+
/** Canonical median price in USD */
39+
@Column({ type: "decimal", precision: 30, scale: 8 })
40+
price: number;
41+
42+
/** Raw prices per source, e.g. { chainlink: 2000.5, band: 2001.0, uniswap_twap: 1999.8 } */
43+
@Column({ type: "jsonb" })
44+
sourcePrices: Record<PriceSource, number>;
45+
46+
/** Whether a >5% deviation was detected between sources */
47+
@Column({ type: "boolean", default: false })
48+
deviationAlert: boolean;
49+
50+
/** Max % deviation observed between sources */
51+
@Column({ type: "decimal", precision: 8, scale: 4, default: 0 })
52+
maxDeviationPercent: number;
53+
54+
@CreateDateColumn()
55+
createdAt: Date;
56+
}

src/blockchain/oracle/oracle.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import { NonceManagementService } from "./services/nonce-management.service";
88
import { SubmitterService } from "./services/submitter.service";
99
import { SubmissionBatchService } from "./services/submission-batch.service";
1010
import { SubmissionVerifierService } from "./submission-verifier.service";
11+
import { PriceFeedService } from "./services/price-feed.service";
12+
import { PriceFeedController } from "./price-feed.controller";
1113
import { SignedPayload } from "./entities/signed-payload.entity";
1214
import { SubmissionNonce } from "./entities/submission-nonce.entity";
15+
import { PriceRecord } from "./entities/price-record.entity";
1316
import { AuditModule } from "src/infrastructure/audit/audit.module";
1417

1518
/**
@@ -18,18 +21,19 @@ import { AuditModule } from "src/infrastructure/audit/audit.module";
1821
*/
1922
@Module({
2023
imports: [
21-
TypeOrmModule.forFeature([SignedPayload, SubmissionNonce]),
24+
TypeOrmModule.forFeature([SignedPayload, SubmissionNonce, PriceRecord]),
2225
ConfigModule,
2326
AuditModule,
2427
],
25-
controllers: [OracleController],
28+
controllers: [OracleController, PriceFeedController],
2629
providers: [
2730
OracleService,
2831
PayloadSigningService,
2932
NonceManagementService,
3033
SubmitterService,
3134
SubmissionBatchService,
3235
SubmissionVerifierService,
36+
PriceFeedService,
3337
],
3438
exports: [
3539
OracleService,
@@ -38,6 +42,7 @@ import { AuditModule } from "src/infrastructure/audit/audit.module";
3842
SubmitterService,
3943
SubmissionBatchService,
4044
SubmissionVerifierService,
45+
PriceFeedService,
4146
],
4247
})
4348
export class OracleModule {}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Controller, Get, Param, Query } from "@nestjs/common";
2+
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
3+
import { PriceFeedService } from "./services/price-feed.service";
4+
import {
5+
GetHistoricalPricesDto,
6+
PriceResponseDto,
7+
} from "./dto/price-feed.dto";
8+
import { SupportedChain } from "./entities/price-record.entity";
9+
10+
@ApiTags("Price Feed")
11+
@Controller("price-feed")
12+
export class PriceFeedController {
13+
constructor(private readonly priceFeedService: PriceFeedService) {}
14+
15+
@Get(":chain/:asset")
16+
@ApiOperation({ summary: "Get current aggregated price for an asset on a chain" })
17+
@ApiResponse({ status: 200, type: PriceResponseDto })
18+
async getCurrentPrice(
19+
@Param("chain") chain: SupportedChain,
20+
@Param("asset") asset: string,
21+
): Promise<PriceResponseDto> {
22+
return this.priceFeedService.getCurrentPrice(asset, chain);
23+
}
24+
25+
@Get(":chain/:asset/history")
26+
@ApiOperation({ summary: "Get historical prices for an asset on a chain" })
27+
@ApiResponse({ status: 200, type: [PriceResponseDto] })
28+
async getHistoricalPrices(
29+
@Param("chain") chain: SupportedChain,
30+
@Param("asset") asset: string,
31+
@Query() query: GetHistoricalPricesDto,
32+
): Promise<PriceResponseDto[]> {
33+
return this.priceFeedService.getHistoricalPrices(
34+
asset,
35+
chain,
36+
query.limit,
37+
);
38+
}
39+
}

0 commit comments

Comments
 (0)