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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ npm run build # tsc -b && vite build && node scripts/build-extension.mjs

All four run in CI (`.github/workflows/ci.yml`) on every push to `main` and on every pull request. The linting step automatically enforces that no network-call APIs (like `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `navigator.sendBeacon`) or Horizon server connections are initiated outside the `src/adapter/` directory.

## Timeout Behavior for getScore

`getScore` now includes a built‑in timeout to prevent the signing flow from hanging indefinitely. The default timeout is **5 seconds** and can be overridden per call via the optional `options` parameter. If the operation exceeds the timeout, the function resolves with a fallback score of `-1`, which is treated as an unknown score and results in a safe warning tier.

You can configure the default timeout by modifying `src/adapter/config.ts` (`DEFAULT_GET_SCORE_TIMEOUT_MS`). Tests use a shorter timeout to verify the fallback behaviour.

## Roadmap

- [x] Popup renders one score across the four tiers. _(stub)_
Expand Down
1 change: 1 addition & 0 deletions src/adapter/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_GET_SCORE_TIMEOUT_MS = 5000;
14 changes: 12 additions & 2 deletions src/adapter/oracleAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { getScore } from './oracleAdapter'

describe('getScore', () => {
Expand All @@ -13,4 +13,14 @@ describe('getScore', () => {
const b = await getScore('SAME')
expect(a).toBe(b)
})
})

it('returns fallback -1 on timeout', async () => {
vi.useFakeTimers();
const promise = getScore('TIMEOUTDEST', { timeoutMs: 50 });
// advance timers past the internal delay (150ms) and timeout (50ms)
vi.advanceTimersByTime(200);
const result = await promise;
expect(result).toBe(-1);
vi.useRealTimers();
});
});
48 changes: 42 additions & 6 deletions src/adapter/oracleAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
import { DEFAULT_GET_SCORE_TIMEOUT_MS } from './config';

/**
* Stand-in for the grydlock-oracle-adapter package's getScore(destination).
* The popup only depends on this function's signature — swap the body for
* the real import once the adapter package is available.
* Retrieves a risk score for a destination with a configurable timeout.
* If the operation exceeds the timeout, it resolves with a fallback score of -1.
*
* @param destination The destination address to score.
* @param options Optional configuration: timeoutMs overrides the default timeout,
* signal allows external cancellation.
*/
export async function getScore(destination: string): Promise<number> {
await new Promise((resolve) => setTimeout(resolve, 150))
return stubScoreFor(destination)
export async function getScore(
destination: string,
options?: { timeoutMs?: number; signal?: AbortSignal }
): Promise<number> {
const timeoutMs = options?.timeoutMs ?? DEFAULT_GET_SCORE_TIMEOUT_MS;
const controller = new AbortController();
const signal = options?.signal ?? controller.signal;

// Create a timeout promise that aborts after the specified duration.
const timeoutPromise = new Promise<never>((_, reject) => {
const id = setTimeout(() => {
// Abort any ongoing work and resolve with fallback.
controller.abort();
reject(new Error('Timeout'));
}, timeoutMs);
// Ensure timeout cleared if operation finishes first.
signal.addEventListener('abort', () => clearTimeout(id));
});

// The actual score computation (stub) wrapped in a promise.
const scorePromise = (async () => {
// Simulate async work (the existing stub delay).
await new Promise((resolve) => setTimeout(resolve, 150));
return stubScoreFor(destination);
})();

try {
const result = await Promise.race([scorePromise, timeoutPromise]);
return result as number;
} catch (e) {

Check failure on line 40 in src/adapter/oracleAdapter.ts

View workflow job for this annotation

GitHub Actions / verify

'e' is defined but never used
// On timeout, return fallback score.
console.warn('getScore timeout for destination', destination);
return -1; // fallback indicating unknown score
}
}

function stubScoreFor(destination: string): number {
Expand Down
2 changes: 1 addition & 1 deletion src/intercept/resolveOutcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Decision, Outcome } from './protocol'

export interface ResolveOutcomeDeps {
extractDestination: (xdr: string) => { destination: string; asset?: string } | null
getScore: (destination: string) => Promise<number>
getScore: (destination: string, options?: { timeoutMs?: number; signal?: AbortSignal }) => Promise<number>
requestDecision: (info: { destination: string; asset?: string; score: number }) => Promise<Decision>
}

Expand Down
Loading