repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | /** |
| #2 | * ooda/observe.ts — Market data adapter |
| #3 | * |
| #4 | * v0: synthesises candles from a seeded RNG (no real data needed). |
| #5 | * Real adapters drop in by replacing `observe()`: |
| #6 | * - Pyth Network price feeds (on-chain, devnet-safe) |
| #7 | * - Helius RPC + getAccountInfo on a Switchboard oracle |
| #8 | * - A DEX REST endpoint (must pass reject_mainnet guard) |
| #9 | * |
| #10 | * The adapter contract: |
| #11 | * observe(state) → Promise<Candle[]> (oldest first, last = current) |
| #12 | */ |
| #13 | |
| #14 | import type { Candle, State } from './state.js'; |
| #15 | |
| #16 | // ─── Mainnet guard ──────────────────────────────────────────────────────────── |
| #17 | |
| #18 | const MAINNET_HOSTNAMES = [ |
| #19 | 'api.mainnet-beta.solana.com', |
| #20 | 'mainnet.helius-rpc.com', |
| #21 | 'mainnet.rpc.jito.wtf', |
| #22 | 'solana-mainnet', |
| #23 | 'mainnet-beta', |
| #24 | ]; |
| #25 | |
| #26 | export function rejectMainnet(rpcUrl: string): void { |
| #27 | if (process.env['MAINNET_OK'] === '1') return; // escape hatch (still no signing path) |
| #28 | for (const host of MAINNET_HOSTNAMES) { |
| #29 | if (rpcUrl.toLowerCase().includes(host)) { |
| #30 | throw new Error( |
| #31 | `[SAFETY] Mainnet RPC URL rejected: "${rpcUrl}". ` + |
| #32 | `v0 only supports devnet. Set MAINNET_OK=1 to bypass (no signing path exists anyway).`, |
| #33 | ); |
| #34 | } |
| #35 | } |
| #36 | } |
| #37 | |
| #38 | // ─── Seeded PRNG (mulberry32) ───────────────────────────────────────────────── |
| #39 | |
| #40 | function mulberry32(seed: number) { |
| #41 | return function () { |
| #42 | seed |= 0; |
| #43 | seed = (seed + 0x6d2b79f5) | 0; |
| #44 | let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); |
| #45 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; |
| #46 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296; |
| #47 | }; |
| #48 | } |
| #49 | |
| #50 | // ─── Synthetic candle generator ─────────────────────────────────────────────── |
| #51 | |
| #52 | export class SynthObserver { |
| #53 | private rand: () => number; |
| #54 | private lastClose: number; |
| #55 | private candles: Candle[] = []; |
| #56 | private readonly windowSize: number; |
| #57 | |
| #58 | constructor(seed = 42, startPrice = 150_000, windowSize = 20) { |
| #59 | this.rand = mulberry32(seed); |
| #60 | this.lastClose = startPrice; |
| #61 | this.windowSize = windowSize; |
| #62 | } |
| #63 | |
| #64 | /** Generate one new candle and return the rolling window */ |
| #65 | tick(now = new Date()): Candle[] { |
| #66 | const move = (this.rand() - 0.48) * 0.03; // slight upward drift |
| #67 | const open = this.lastClose; |
| #68 | const close = Math.round(open * (1 + move)); |
| #69 | const high = Math.round(Math.max(open, close) * (1 + this.rand() * 0.01)); |
| #70 | const low = Math.round(Math.min(open, close) * (1 - this.rand() * 0.01)); |
| #71 | const volume = Math.round(1_000_000 + this.rand() * 9_000_000); |
| #72 | |
| #73 | this.candles.push({ t: now.toISOString(), o: open, h: high, l: low, c: close, v: volume }); |
| #74 | if (this.candles.length > this.windowSize) this.candles.shift(); |
| #75 | this.lastClose = close; |
| #76 | return [...this.candles]; |
| #77 | } |
| #78 | } |
| #79 | |
| #80 | // ─── Real Helius/Pyth adapter skeleton ─────────────────────────────────────── |
| #81 | |
| #82 | export async function observeFromHelius( |
| #83 | rpcUrl: string, |
| #84 | state: State, |
| #85 | windowSize = 20, |
| #86 | ): Promise<Candle[]> { |
| #87 | rejectMainnet(rpcUrl); |
| #88 | |
| #89 | // Stub: in a real implementation, fetch from a Pyth price account via |
| #90 | // getAccountInfo, decode the PriceData struct, and push a candle. |
| #91 | // For now, fall back to synth so the harness can still run. |
| #92 | console.warn('[observe] Helius adapter not yet wired — using synth candles'); |
| #93 | const synth = new SynthObserver(state.tick, 150_000, windowSize); |
| #94 | for (let i = 0; i < Math.min(state.tick, windowSize); i++) synth.tick(); |
| #95 | return synth.tick(); |
| #96 | } |
| #97 | |
| #98 | // ─── Staleness check ────────────────────────────────────────────────────────── |
| #99 | |
| #100 | /** Returns true if the most recent candle is older than maxAgeSeconds */ |
| #101 | export function isStale(candles: Candle[], maxAgeSeconds = 60): boolean { |
| #102 | if (candles.length === 0) return true; |
| #103 | const last = candles[candles.length - 1]!; |
| #104 | const age = (Date.now() - new Date(last.t).getTime()) / 1000; |
| #105 | return age > maxAgeSeconds; |
| #106 | } |
| #107 |