Skip to content

feat(automation): connect live oracle prices to automation price triggers#205

Merged
KevinMB0220 merged 3 commits intoGalaxy-KJ:mainfrom
salazarsebas:feat/issue-155-oracle-price-triggers
Mar 30, 2026
Merged

feat(automation): connect live oracle prices to automation price triggers#205
KevinMB0220 merged 3 commits intoGalaxy-KJ:mainfrom
salazarsebas:feat/issue-155-oracle-price-triggers

Conversation

@salazarsebas
Copy link
Copy Markdown
Contributor

@salazarsebas salazarsebas commented Mar 30, 2026

Summary

This PR connects live oracle prices to automation price triggers so active TriggerType.PRICE rules can evaluate against current market data instead of static context.

Closes #155

What Changed

  • inject optional OracleAggregator support into AutomationService
  • add PriceConditionContext to ExecutionContext
  • move price evaluation in ConditionEvaluator to consume live context instead of querying infrastructure directly
  • add checkAndExecute(), startPriceMonitoring(), and stopPriceMonitoring() for active price rules
  • fetch and reuse aggregated prices per monitoring cycle for all referenced assets
  • add tests for price-above, price-below, monitoring, and no-oracle fallback
  • add package-level Jest coverage config and document price-trigger usage

Design Notes

ConditionEvaluator is now context-driven for price conditions. This keeps evaluation logic pure and lets AutomationService own oracle access, price prefetching, and monitoring lifecycle management.

When no oracle is configured, non-price automations continue to work as before. Price conditions evaluate to false unless a caller provides priceContext explicitly.

Validation

  • npx jest packages/core/automation/src/test/condition.test.ts packages/core/automation/src/test/automation.test.ts --runInBand
  • cd packages/core/automation && npm run lint
  • cd packages/core/automation && npm run test:coverage

Notes For Maintainers

The repository currently has unrelated pre-existing build/type-check issues in other packages (core-invisible-wallet and core-stellar-sdk). This PR does not modify those areas.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added live price monitoring for automations with price-based triggers
    • Price conditions now evaluate against real-time market data
    • Automations can trigger automatically when asset prices cross specified thresholds
  • Documentation

    • Added comprehensive package documentation for the automation service
  • Tests

    • Configured Jest testing framework with code coverage reporting
    • Added test coverage for new price monitoring capabilities

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

This PR integrates oracle-backed price feeds into AutomationService, enabling price-based automations (PRICE_ABOVE/PRICE_BELOW conditions) to evaluate against live asset prices. The implementation adds optional oracle injection, introduces a price monitoring polling loop, and extends the execution context with resolved price data.

Changes

Cohort / File(s) Summary
Documentation & Configuration
packages/core/automation/README.md, jest.config.cjs, package.json
Added package documentation explaining oracle-backed price evaluation workflow; configured Jest for TypeScript testing with coverage thresholds and oracle module mapping; added test:coverage npm script.
Type Definitions
packages/core/automation/src/types/automation-types.ts
Introduced PriceConditionContext interface with price map and timestamp; extended ExecutionContext with optional priceContext field to carry oracle-resolved prices into condition evaluation.
Core Service Implementation
packages/core/automation/src/services/automation.service.ts
Added optional oracle field and priceMonitorInterval; introduced three public methods (startPriceMonitoring(), stopPriceMonitoring(), checkAndExecute()) to manage price polling; refactored executeRule() to asynchronously enrich context with live prices via attachLivePrices(); added internal helpers for price context construction and asset extraction; updated shutdown flow to stop monitoring.
Condition Evaluation
packages/core/automation/src/utils/condition-evaluator.ts
Removed oracle dependency from ConditionEvaluator; updated evaluatePriceCondition() to accept optional PriceConditionContext and resolve prices with case-insensitive asset lookups; added resolvePrice() helper; returns false for unavailable prices instead of throwing.
Test Suite
packages/core/automation/src/test/automation.test.ts, condition.test.ts
Added mocks for Supabase and oracle price feeds; introduced createPriceRule() and createPriceContext() helpers; expanded automation tests to cover price-triggered rule execution with oracle, execution without oracle, and price monitoring lifecycle (start/stop/interval validation); migrated condition tests to use context-provided prices instead of oracle-backed evaluation.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant AutoSvc as AutomationService
    participant Oracle as OracleAggregator
    participant Monitor as Polling Loop
    participant Evaluator as ConditionEvaluator
    participant Rules as Rules DB

    App->>AutoSvc: startPriceMonitoring(30000)
    activate AutoSvc
    AutoSvc->>Monitor: initialize interval
    activate Monitor
    
    Monitor->>AutoSvc: runPriceMonitoringCycle()
    AutoSvc->>Rules: find active PRICE rules
    Rules-->>AutoSvc: [rule1, rule2, ...]
    
    AutoSvc->>AutoSvc: collectPriceAssets([rule1, rule2])
    AutoSvc->>Oracle: getAggregatedPrices(['XLM', 'USDC'])
    Oracle-->>AutoSvc: {XLM: 0.15, USDC: 1.0, timestamp}
    
    AutoSvc->>AutoSvc: buildPriceContext() → priceContext
    
    loop For each rule
        AutoSvc->>AutoSvc: attachLivePrices(rule, baseContext)
        AutoSvc->>Evaluator: evaluateConditionGroup(conditions, context+priceContext)
        rect rgba(100, 150, 255, 0.5)
            Evaluator->>Evaluator: evaluatePriceCondition(PRICE_ABOVE, priceContext)
            Evaluator->>Evaluator: resolvePrice('XLM', priceContext)
        end
        Evaluator-->>AutoSvc: true/false
        
        alt Condition matched
            AutoSvc->>AutoSvc: executeRule(rule, context)
        end
    end
    
    deactivate Monitor
    deactivate AutoSvc
    
    App->>AutoSvc: stopPriceMonitoring()
    Monitor->>Monitor: clearInterval()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • KevinMB0220

🐰 The prices now flow, through oracle's glow,
AutomationService learns to grow,
With polling loops and contexts bright,
Price triggers dance in the moonlight! 🌙✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers the main changes, design notes, and validation steps, but is missing several required documentation checklist items from the template. Complete the documentation checklist by explicitly marking items complete or incomplete; ensure 'packages/core/automation/README.md' updates and other required sections are verified and checked off.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(automation): connect live oracle prices to automation price triggers' clearly and accurately describes the main change in the changeset.
Linked Issues check ✅ Passed All major objectives from issue #155 are met: oracle injection [#155], price context [#155], monitoring lifecycle [#155], backward compatibility [#155], tests [#155], and documentation [#155].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing issue #155 requirements: oracle integration, price context, condition evaluation, monitoring, tests, and documentation for the automation package.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/core/automation/src/services/automation.service.ts (1)

21-21: Cross-package relative import is fragile.

The relative import path ../../../stellar-sdk/src/utils/supabase-client.js traverses outside the current package and directly imports from another package's src directory. This couples the build/runtime to a specific monorepo directory structure and bypasses the package's public API.

Consider importing from the published package name (e.g., @galaxy-kj/core-stellar-sdk) or re-exporting the client from an appropriate location within the automation package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/automation/src/services/automation.service.ts` at line 21, The
current cross-package relative import of supabaseClient (import of
'../../../stellar-sdk/src/utils/supabase-client.js') is fragile; replace it by
importing the client from the stellar SDK's public package export (e.g., import
{ supabaseClient } from '@galaxy-kj/core-stellar-sdk') or consume a re-export
from the automation package; update the import in automation.service.ts to
reference the published package name (or add a re-export in the stellar SDK
package index and import that) so you no longer reference another package's src
tree directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/automation/src/services/automation.service.ts`:
- Around line 355-362: The current Promise.all over activePriceRules causes all
executeRule(rule.id, {...}) calls to start concurrently and can bypass the
per-rule maxConcurrentExecutions check (because activeExecutions is incremented
concurrently); change this to a controlled-concurrency pattern by serializing or
limiting parallel invocations: either use a concurrency limiter like p-limit and
wrap each call with limit(() => this.executeRule(...)) or iterate the
activePriceRules with an async for-loop and await each executeRule (or process
in configurable batches) so that the effective number of concurrent executeRule
executions respects maxConcurrentExecutions and updates to activeExecutions
occur under the intended limit. Ensure references to activePriceRules,
executeRule, maxConcurrentExecutions, and activeExecutions are updated
accordingly.

In `@packages/core/automation/src/test/automation.test.ts`:
- Around line 549-571: The test's use of expect.not.objectContaining({
priceContext: expect.anything() }) is too lenient because it treats a
present-but-undefined priceContext the same as absent; change the assertion to
explicitly verify the argument has no priceContext property: after calling
serviceWithoutOracle.executeRule, read the first call arg from
mockConditionEvaluator.evaluateConditionGroup.mock.calls (e.g., const arg =
mockConditionEvaluator.evaluateConditionGroup.mock.calls[0][0]) and assert
expect(arg).not.toHaveProperty('priceContext') so the test guarantees
priceContext is strictly absent when evaluateConditionGroup is invoked for
serviceWithoutOracle.

---

Nitpick comments:
In `@packages/core/automation/src/services/automation.service.ts`:
- Line 21: The current cross-package relative import of supabaseClient (import
of '../../../stellar-sdk/src/utils/supabase-client.js') is fragile; replace it
by importing the client from the stellar SDK's public package export (e.g.,
import { supabaseClient } from '@galaxy-kj/core-stellar-sdk') or consume a
re-export from the automation package; update the import in
automation.service.ts to reference the published package name (or add a
re-export in the stellar SDK package index and import that) so you no longer
reference another package's src tree directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74998809-b0a8-421a-995b-66fc07693f31

📥 Commits

Reviewing files that changed from the base of the PR and between f45c7a6 and c430cda.

📒 Files selected for processing (8)
  • packages/core/automation/README.md
  • packages/core/automation/jest.config.cjs
  • packages/core/automation/package.json
  • packages/core/automation/src/services/automation.service.ts
  • packages/core/automation/src/test/automation.test.ts
  • packages/core/automation/src/test/condition.test.ts
  • packages/core/automation/src/types/automation-types.ts
  • packages/core/automation/src/utils/condition-evaluator.ts

Comment on lines +355 to +362
return Promise.all(
activePriceRules.map(rule =>
this.executeRule(rule.id, {
...contextData,
priceContext,
})
)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Concurrent execution of price rules may bypass maxConcurrentExecutions limit.

Promise.all executes all active price rules simultaneously. If there are many active price rules, this could exceed maxConcurrentExecutions (checked per-rule in executeRule but each call increments activeExecutions concurrently before any checks complete).

Consider using a controlled concurrency pattern (e.g., p-limit or sequential execution) or batching to respect the configured limit.

🛠️ Example using sequential execution
-    return Promise.all(
-      activePriceRules.map(rule =>
-        this.executeRule(rule.id, {
-          ...contextData,
-          priceContext,
-        })
-      )
-    );
+    const results: ExecutionResult[] = [];
+    for (const rule of activePriceRules) {
+      const result = await this.executeRule(rule.id, {
+        ...contextData,
+        priceContext,
+      });
+      results.push(result);
+    }
+    return results;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/automation/src/services/automation.service.ts` around lines 355
- 362, The current Promise.all over activePriceRules causes all
executeRule(rule.id, {...}) calls to start concurrently and can bypass the
per-rule maxConcurrentExecutions check (because activeExecutions is incremented
concurrently); change this to a controlled-concurrency pattern by serializing or
limiting parallel invocations: either use a concurrency limiter like p-limit and
wrap each call with limit(() => this.executeRule(...)) or iterate the
activePriceRules with an async for-loop and await each executeRule (or process
in configurable batches) so that the effective number of concurrent executeRule
executions respects maxConcurrentExecutions and updates to activeExecutions
occur under the intended limit. Ensure references to activePriceRules,
executeRule, maxConcurrentExecutions, and activeExecutions are updated
accordingly.

Comment on lines +549 to +571
it('should fall back cleanly when a price rule has no oracle configured', async () => {
const serviceWithoutOracle = new AutomationService({
network: testNetwork,
sourceSecret: 'SBXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
});
const rule = createPriceRule({ id: 'price-rule-no-oracle' });

mockConditionEvaluator.evaluateConditionGroup.mockResolvedValue(false);

await serviceWithoutOracle.registerRule(rule);
const result = await serviceWithoutOracle.executeRule(rule.id);

expect(result.success).toBe(false);
expect(result.error?.message).toBe('Conditions not met');
expect(
mockConditionEvaluator.evaluateConditionGroup
).toHaveBeenCalledWith(
rule.conditionGroup,
expect.not.objectContaining({
priceContext: expect.anything(),
})
);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how expect.not.objectContaining behaves with undefined values
cat << 'EOF' > /tmp/jest-matcher-test.js
const obj1 = { ruleId: 'test', priceContext: undefined };
const obj2 = { ruleId: 'test' };

// Test if expect.not.objectContaining catches undefined
try {
  expect(obj1).toEqual(expect.not.objectContaining({ priceContext: expect.anything() }));
  console.log("PASS: obj1 with priceContext: undefined passed not.objectContaining");
} catch (e) {
  console.log("FAIL: obj1 with priceContext: undefined failed not.objectContaining");
}

try {
  expect(obj2).toEqual(expect.not.objectContaining({ priceContext: expect.anything() }));
  console.log("PASS: obj2 without priceContext passed not.objectContaining");
} catch (e) {
  console.log("FAIL: obj2 without priceContext failed not.objectContaining");
}
EOF
node -e "
const obj1 = { ruleId: 'test', priceContext: undefined };
const obj2 = { ruleId: 'test' };

// expect.anything() matches any value except null/undefined
console.log('priceContext in obj1:', 'priceContext' in obj1);
console.log('obj1.priceContext:', obj1.priceContext);
console.log('priceContext in obj2:', 'priceContext' in obj2);
"

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 150


🏁 Script executed:

npm list jest 2>/dev/null | head -5

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 118


🏁 Script executed:

# Find the test file mentioned in the review
find . -type f -name "automation.test.ts" | head -5

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 121


🏁 Script executed:

# Check the test file around lines 549-571
sed -n '540,575p' ./packages/core/automation/src/test/automation.test.ts

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 1209


🏁 Script executed:

# Check Jest version and test setup
cat ./packages/core/package.json | grep -A 5 '"jest"' | head -10

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 127


🏁 Script executed:

# Find package.json files in the packages directory
find ./packages -name "package.json" -type f | head -10

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 456


🏁 Script executed:

# Check Jest version in the automation package
cat ./packages/core/automation/package.json | grep -E '"jest"|"@jest' -A 1

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 178


🏁 Script executed:

# Create a Jest test to verify the matcher behavior
mkdir -p /tmp/jest-test && cd /tmp/jest-test

# Create a simple test file
cat > test-matcher.test.js << 'EOF'
describe('expect.not.objectContaining matcher behavior', () => {
  test('should FAIL when priceContext is undefined', () => {
    const objWithUndefined = { ruleId: 'test', priceContext: undefined };
    expect(objWithUndefined).toEqual(
      expect.not.objectContaining({
        priceContext: expect.anything(),
      })
    );
  });

  test('should PASS when priceContext is absent', () => {
    const objWithout = { ruleId: 'test' };
    expect(objWithout).toEqual(
      expect.not.objectContaining({
        priceContext: expect.anything(),
      })
    );
  });
});
EOF

# Create minimal jest.config.js
cat > jest.config.js << 'EOF'
module.exports = {
  testEnvironment: 'node',
};
EOF

# Create package.json
cat > package.json << 'EOF'
{
  "name": "jest-test",
  "version": "1.0.0",
  "devDependencies": {
    "jest": "^30.0.5"
  }
}
EOF

# Install jest
npm install --silent 2>&1 | grep -v "npm warn"

# Run the test
npm test -- test-matcher.test.js 2>&1 | grep -A 50 "FAIL\|PASS\|●"

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 49


🏁 Script executed:

# Let's check if we can run the actual test file from the repo
cd ./packages/core/automation
npm test -- --testNamePattern="should fall back cleanly when a price rule has no oracle configured" 2>&1 | tail -50

Repository: Galaxy-KJ/Galaxy-DevKit

Length of output: 224


🌐 Web query:

Jest expect.objectContaining expect.anything() undefined behavior matcher

💡 Result:

In Jest, expect.anything matches any value except null or undefined. When used within expect.objectContaining({ prop: expect.anything }), it requires the object to have a prop property whose value is neither null nor undefined. If the property is undefined (i.e., absent or explicitly set to undefined), the matcher fails. This is the documented behavior: official Jest docs state expect.anything matches "anything but null or undefined" [3,4,14]. Stack Overflow confirms it does not match undefined, even in object contexts [2]. Blogs note expect.objectContaining does not natively support undefined properties; workarounds like jest-extended's expect.toBeOneOf([expect.anything, null, undefined]) are needed [1,9]. Examples: - Passes: { prop: "value" } matches { prop: expect.anything } - Fails: { prop: undefined } or {} does not match { prop: expect.anything } For optional/undefined properties, omit from objectContaining, use toHaveProperty('prop') without value, or extend with custom matchers.

Citations:


The test assertion is subtly lenient with expect.anything().

The use of expect.not.objectContaining({ priceContext: expect.anything() }) will pass when priceContext is either absent OR explicitly undefined. Since expect.anything() matches any value except null/undefined, the objectContaining part won't match either case, making expect.not.objectContaining pass in both scenarios.

If the intent is to ensure priceContext is strictly absent (not present-but-undefined), consider using a more explicit assertion:

  • expect.not.objectContaining({ priceContext: expect.anything() }) — current (accepts both absent and undefined)
  • Alternative: Check the exact object structure or use a custom matcher if stricter behavior is needed

This doesn't necessarily indicate a bug, but it's worth confirming whether the current behavior aligns with the intended guarantee that no price context is provided to the condition evaluator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/automation/src/test/automation.test.ts` around lines 549 - 571,
The test's use of expect.not.objectContaining({ priceContext: expect.anything()
}) is too lenient because it treats a present-but-undefined priceContext the
same as absent; change the assertion to explicitly verify the argument has no
priceContext property: after calling serviceWithoutOracle.executeRule, read the
first call arg from mockConditionEvaluator.evaluateConditionGroup.mock.calls
(e.g., const arg =
mockConditionEvaluator.evaluateConditionGroup.mock.calls[0][0]) and assert
expect(arg).not.toHaveProperty('priceContext') so the test guarantees
priceContext is strictly absent when evaluateConditionGroup is invoked for
serviceWithoutOracle.

@KevinMB0220 KevinMB0220 self-requested a review March 30, 2026 05:51
@KevinMB0220 KevinMB0220 merged commit c9faf56 into Galaxy-KJ:main Mar 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Connect Oracle price feeds to Automation price triggers

2 participants