Skip to content
22 changes: 19 additions & 3 deletions packages/euler-v2-sdk/docs/caching-external-data-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ type BuildQueryFn = <T extends (...args: any[]) => Promise<any>>(
queryName: string,
fn: T,
target: object,
// null is an explicit instruction to call fn without caching
context?: { getCacheKey: (args: unknown[]) => string | null },
) => T;
```

`context.getCacheKey(args)` returns a deterministic string for cacheable calls
and `null` for calls that must bypass caching. `null` is a semantic no-cache
signal, not a missing key: custom builders must call `fn` directly instead of
replacing it with a generic serialized key. Activity cursor pages use this
contract so pagination does not grow a long-lived cache entry per cursor.

Pass `buildQuery` once when building the SDK and it propagates to every service and adapter:

```typescript
Expand Down Expand Up @@ -93,19 +100,28 @@ export const queryClient = new QueryClient({
export const sdkBuildQuery: BuildQueryFn = (queryName, fn, _target, context) => {
const staleTime = STALE_TIMES[queryName] ?? DEFAULT_STALE_TIME;

const wrapped = (...args: unknown[]) =>
queryClient.fetchQuery({
queryKey: ["sdk", queryName, context?.getCacheKey(args) ?? serializeQueryArgs(args)],
const wrapped = (...args: unknown[]) => {
const cacheKey = context
? context.getCacheKey(args)
: serializeQueryArgs(args);
if (cacheKey === null) return fn(...args);

return queryClient.fetchQuery({
queryKey: ["sdk", queryName, cacheKey],
queryFn: () => fn(...args),
staleTime,
});
};

return wrapped as typeof fn;
};
```

Each `query*` call becomes a `fetchQuery` with a deterministic cache key derived from the query name and the SDK-provided `context.getCacheKey(args)` helper. Generic keys remove provider-object noise and normalize address casing. Query-owned `getQueryKeyX` methods handle query-specific semantics such as unordered feed sets or asset filters while ordered payloads remain order-sensitive. If the cached value is fresh (within `staleTime`), no network call is made.

Calls whose key helper returns `null` invoke the underlying fetcher directly and
do not create a react-query cache entry.

### Central stale time settings

```typescript
Expand Down
10 changes: 10 additions & 0 deletions packages/euler-v2-sdk/docs/config-through-env.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ RPC URLs use `config.rpcUrls` or `EULER_SDK_RPC_URL_<chainId>`.
| `queryCacheEnabled` | `EULER_SDK_QUERY_CACHE_ENABLED` | `true` |
| `queryCacheTtlMs` | `EULER_SDK_QUERY_CACHE_TTL_MS` | `5000` |

## Activity

| Config field | Environment variable | Default |
|---|---|---|
| `activityV3ApiUrl` | `EULER_SDK_ACTIVITY_V3_API_URL` | `v3ApiUrl` |
| `activityV3ApiKey` | `EULER_SDK_ACTIVITY_V3_API_KEY` | `v3ApiKey` |

The activity-specific values configure the normalized account and vault event
endpoints without changing the V3 configuration used by other SDK services.

## Account Service

| Config field | Environment variable | Default |
Expand Down
10 changes: 9 additions & 1 deletion packages/euler-v2-sdk/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const sdk = await buildEulerSDK({
},
v3ApiUrl: "https://your-v3-api",
v3ApiKey: process.env.EULER_SDK_V3_API_KEY,
activityV3ApiUrl: process.env.EULER_SDK_ACTIVITY_V3_API_URL,
activityV3ApiKey: process.env.EULER_SDK_ACTIVITY_V3_API_KEY,
swapApiUrl: "https://swap.euler.finance",
swapDefaultDeadline: 1800,
},
Expand Down Expand Up @@ -73,14 +75,20 @@ Use explicit nested service config when the option is a function or a custom obj
| `swapServiceConfig` | Euler swap API | Swap quote fetching |
| `rewardsServiceConfig` | `v3` adapter with direct Brevis/Fuul helper reads | Reward campaign data, per-user rewards, and reward claim planning |
| `intrinsicApyServiceConfig` | V3 intrinsic APY API | Underlying yield data for vault assets |
| `activityServiceConfig` | V3 activity API | Normalized account and vault event timelines with coverage metadata |
| `buildQuery` | 5s in-memory cache | Wrap all external queries for caching, logging, or profiling |
| `queryCacheConfig` | `{ enabled: true, ttlMs: 5000, failureTtlMs: 5000 }` | Built-in query cache settings when `buildQuery` is not supplied |
| `plugins` | `[]` | Extend on-chain reads and transaction plans |
| `servicesOverrides` | `{}` | Replace any built-in service with a custom implementation |

## V3 adapter config

When `accountServiceConfig.adapter`, `eVaultServiceConfig.adapter`, `eulerEarnServiceConfig.adapter`, `vaultTypeAdapterConfig`, `rewardsServiceConfig.adapter`, `intrinsicApyServiceConfig`, or the built-in pricing service use V3, the SDK forwards the resolved API key as an `X-API-Key` request header.
When `accountServiceConfig.adapter`, `eVaultServiceConfig.adapter`, `eulerEarnServiceConfig.adapter`, `vaultTypeAdapterConfig`, `rewardsServiceConfig.adapter`, `intrinsicApyServiceConfig`, `activityServiceConfig`, or the built-in pricing service use V3, the SDK forwards the resolved API key as an `X-API-Key` request header.

Activity uses the shared `v3ApiUrl` and `v3ApiKey` by default. Set
`activityV3ApiUrl` and `activityV3ApiKey`, or their
`EULER_SDK_ACTIVITY_V3_API_URL` and `EULER_SDK_ACTIVITY_V3_API_KEY`
environment variables, when activity requests use a separate endpoint or key.

Adapter-specific keys override the shared key within the same configuration layer. Layer priority still applies, so `config.pricingApiKey` overrides `pricingServiceConfig.apiKey`, and `pricingServiceConfig.apiKey` overrides `EULER_SDK_PRICING_API_KEY`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,19 @@ export const sdkBuildQuery: BuildQueryFn = (queryName, fn, _target, context) =>

const wrapped = async (...args: unknown[]) => {
recordExecution(queryName);
const queryKey = [
"sdk",
queryName,
context?.getCacheKey(args) ?? serializeQueryArgs(args),
] as QueryKey;
const cacheKey = context
? context.getCacheKey(args)
: serializeQueryArgs(args);
if (cacheKey === null) {
try {
return await interceptedFetcher(...args);
} catch (error) {
recordFailure(queryName);
throw error;
}
}

const queryKey = ["sdk", queryName, cacheKey] as QueryKey;
const { disableCache, fetchQueryOptions: overrides } = getQueryBuildOverrides();

const fetchOptions: FetchQueryOptions<unknown, Error, unknown, QueryKey> = {
Expand Down
21 changes: 11 additions & 10 deletions packages/euler-v2-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from "./services/oracleAdapterService/index.js";
export * from "./services/feeFlowService/index.js";
export * from "./services/reulLockService/index.js";
export * from "./services/positionMigrationService/index.js";
export * from "./services/activityService/index.js";

// Plugins
export * from "./plugins/index.js";
Expand All @@ -44,16 +45,16 @@ export * from "./utils/subAccounts.js";
export * from "./utils/accountPositionClassification.js";
export { VaultType } from "./utils/types.js";
export {
type BuildQueryFn,
type BuildQueryContext,
type QueryCacheConfig,
applyBuildQuery,
createQueryCacheBuildQuery,
getEulerSdkQueryKey,
normalizeQueryKeyObjectSets,
normalizeQueryKeySet,
normalizeQueryKeyValue,
serializeQueryArgs,
type BuildQueryFn,
type BuildQueryContext,
type QueryCacheConfig,
applyBuildQuery,
createQueryCacheBuildQuery,
getEulerSdkQueryKey,
normalizeQueryKeyObjectSets,
normalizeQueryKeySet,
normalizeQueryKeyValue,
serializeQueryArgs,
} from "./utils/buildQuery.js";
export type { EulerSDKQueryName, QueryMethodName } from "./utils/queryNames.js";
export * from "./utils/stateOverrides/index.js";
Expand Down
38 changes: 38 additions & 0 deletions packages/euler-v2-sdk/src/sdk/buildSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import {
defaultEulerEarnV3AdapterConfig,
defaultEulerLabelsURLAdapterConfig,
defaultIntrinsicApyV3AdapterConfig,
defaultActivityV3AdapterConfig,
defaultPricingServiceConfig,
defaultRewardsV3AdapterConfig,
defaultSwapServiceConfig,
Expand Down Expand Up @@ -127,6 +128,13 @@ import {
type MorphoMigrationConnectorConfig,
type PositionMigrationServiceConfig,
} from "../services/positionMigrationService/index.js";
import {
ActivityService,
ActivityV3Adapter,
UnavailableActivityAdapter,
type ActivityServiceConfig,
type IActivityService,
} from "../services/activityService/index.js";
import {
type EulerSDKConfig,
readEulerSDKEnvConfig,
Expand Down Expand Up @@ -191,6 +199,7 @@ export interface BuildSDKOverrides<
feeFlowService?: IFeeFlowService;
reulLockService?: IREULLockService;
positionMigrationService?: IPositionMigrationService;
activityService?: IActivityService;
}

export type { EulerSDKConfig } from "./config.js";
Expand Down Expand Up @@ -228,6 +237,8 @@ export interface BuildSDKOptions<
aave?: AaveMigrationConnectorConfig;
metamorpho?: MetamorphoMigrationConnectorConfig;
};
/** Configuration for the built-in V3 activity adapter. */
activityServiceConfig?: ActivityServiceConfig;
/** Default in-memory cache applied to all decorated `query*` methods. Enabled by default with 5s success and failure TTLs. */
queryCacheConfig?: QueryCacheConfig;
/** Optional query decorator applied to all query* functions across all services. Use for global logging, caching, profiling, etc. */
Expand Down Expand Up @@ -538,6 +549,7 @@ export async function buildEulerSDK<
feeFlowServiceConfig,
positionMigrationServiceConfig,
positionMigrationConnectorConfig,
activityServiceConfig,
} = options;

const envConfig = readEulerSDKEnvConfig();
Expand Down Expand Up @@ -1532,6 +1544,31 @@ export async function buildEulerSDK<
resolvedBuildQuery,
);
})();
const resolvedActivityV3Config = resolveV3AdapterConfig(
defaultActivityV3AdapterConfig,
{
explicitConfig: activityServiceConfig,
explicitV3ApiKey: v3ApiKey,
envConfig,
config,
envEndpoint: envConfig.activityV3ApiUrl,
configEndpoint: config?.activityV3ApiUrl,
envApiKey: envConfig.activityV3ApiKey,
configApiKey: config?.activityV3ApiKey,
},
);
const canBuildActivityV3 =
resolvedActivityV3Config.endpoint.trim().length > 0;
const activityService =
servicesOverrides?.activityService ??
new ActivityService(
disableV3
? new UnavailableActivityAdapter("v3-disabled")
: canBuildActivityV3
? new ActivityV3Adapter(resolvedActivityV3Config)
: new UnavailableActivityAdapter("source-not-configured"),
resolvedBuildQuery,
Comment thread
Seranged marked this conversation as resolved.
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (executionService instanceof ExecutionService) {
executionService.setProviderService(providerService as ProviderService);
Expand Down Expand Up @@ -1626,6 +1663,7 @@ export async function buildEulerSDK<
feeFlowService,
reulLockService,
positionMigrationService,
activityService,
plugins,
});

Expand Down
29 changes: 19 additions & 10 deletions packages/euler-v2-sdk/src/sdk/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ export interface EulerSDKConfig {
v3ApiUrl?: string;
v3ApiKey?: string;
/**
* Globally disables the V3 HTTP adapter as the primary in all fallback chains.
* When true, services configured for `fallback` use the secondary adapter only
* (onchain / direct / subgraph). Per-service `adapter` overrides still apply.
* Disables built-in V3 HTTP adapters. Services with secondary adapters use
* those sources; V3-only services report an unavailable capability.
*/
disableV3?: boolean;

Expand Down Expand Up @@ -102,6 +101,9 @@ export interface EulerSDKConfig {
feeFlowControllerUtilAddress?: Address;
feeFlowDefaultBuyDeadlineSeconds?: number;

activityV3ApiUrl?: string;
activityV3ApiKey?: string;

queryCacheEnabled?: boolean;
queryCacheTtlMs?: number;
}
Expand Down Expand Up @@ -236,7 +238,10 @@ function readOptionalAddress(
}
}

function readOptionalString(value: unknown, message: string): string | undefined {
function readOptionalString(
value: unknown,
message: string,
): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== "string") throw new Error(message);
return value;
Expand Down Expand Up @@ -268,12 +273,13 @@ function readTurtleRewardToken(
token.address,
`${name} rewardToken.address values must be EVM addresses`,
),
chainId: token.chainId === undefined
? undefined
: readPositiveInteger(
token.chainId,
`${name} rewardToken.chainId values must be positive integers`,
),
chainId:
token.chainId === undefined
? undefined
: readPositiveInteger(
token.chainId,
`${name} rewardToken.chainId values must be positive integers`,
),
symbol: readOptionalString(
token.symbol,
`${name} rewardToken.symbol values must be strings`,
Expand Down Expand Up @@ -548,6 +554,9 @@ export function readEulerSDKEnvConfig(
"EULER_SDK_FEE_FLOW_DEFAULT_BUY_DEADLINE_SECONDS",
),

activityV3ApiUrl: readString(env, "EULER_SDK_ACTIVITY_V3_API_URL"),
activityV3ApiKey: readString(env, "EULER_SDK_ACTIVITY_V3_API_KEY"),

queryCacheEnabled: readBoolean(env, "EULER_SDK_QUERY_CACHE_ENABLED"),
queryCacheTtlMs: readNumber(env, "EULER_SDK_QUERY_CACHE_TTL_MS"),
});
Expand Down
8 changes: 6 additions & 2 deletions packages/euler-v2-sdk/src/sdk/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { TokenlistServiceConfig } from "../services/tokenlistService/index.
import type { PricingServiceConfig } from "../services/priceService/index.js";
import type { IntrinsicApyV3AdapterConfig } from "../services/intrinsicApyService/index.js";
import type { RewardsV3AdapterConfig } from "../services/rewardsService/index.js";
import type { ActivityServiceConfig } from "../services/activityService/index.js";

const SUBGRAPH_BASE_URL =
"https://api.goldsky.com/api/public/project_cm4iagnemt1wp01xn4gh1agft/subgraphs";
Expand Down Expand Up @@ -68,6 +69,10 @@ export const defaultRewardsV3AdapterConfig: RewardsV3AdapterConfig = {
endpoint: DEFAULT_V3_API_URL,
};

export const defaultActivityV3AdapterConfig: ActivityServiceConfig = {
endpoint: DEFAULT_V3_API_URL,
};

/** Same subgraph endpoints as account vaults; kept for explicit subgraph-based vault type resolution. */
export const defaultVaultTypeSubgraphAdapterConfig: VaultTypeSubgraphAdapterConfig =
defaultAccountVaultsAdapterConfig;
Expand Down Expand Up @@ -106,8 +111,7 @@ export const defaultDeploymentServiceConfig: DeploymentServiceConfig = {
"https://raw.githubusercontent.com/euler-xyz/euler-interfaces/refs/heads/master/EulerChains.json",
};

export const DEFAULT_TOKENLIST_API_BASE_URL =
DEFAULT_V3_API_URL;
export const DEFAULT_TOKENLIST_API_BASE_URL = DEFAULT_V3_API_URL;

export const defaultTokenlistServiceConfig: TokenlistServiceConfig = {
getTokenListUrl: (chainId: number) =>
Expand Down
12 changes: 12 additions & 0 deletions packages/euler-v2-sdk/src/sdk/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import type { IOracleAdapterService } from "../services/oracleAdapterService/ind
import type { IFeeFlowService } from "../services/feeFlowService/index.js";
import type { IREULLockService } from "../services/reulLockService/index.js";
import type { IPositionMigrationService } from "../services/positionMigrationService/index.js";
import {
ActivityService,
UnavailableActivityAdapter,
type IActivityService,
} from "../services/activityService/index.js";
import type { EulerPlugin, PluginPrefetchData } from "../plugins/types.js";
import type { TransactionPlan } from "../services/executionService/executionServiceTypes.js";
import type { AddressOrAccount } from "../entities/Account.js";
Expand Down Expand Up @@ -51,6 +56,7 @@ export interface EulerSDKOptions<
feeFlowService: IFeeFlowService;
reulLockService: IREULLockService;
positionMigrationService: IPositionMigrationService;
activityService?: IActivityService;
plugins?: EulerPlugin[];
}

Expand All @@ -76,6 +82,7 @@ export class EulerSDK<TVaultEntity extends IVaultEntity = VaultEntity> {
public readonly feeFlowService: IFeeFlowService;
public readonly reulLockService: IREULLockService;
public readonly positionMigrationService: IPositionMigrationService;
public readonly activityService: IActivityService;
public readonly plugins: EulerPlugin[];

constructor(options: EulerSDKOptions<TVaultEntity>) {
Expand All @@ -100,6 +107,11 @@ export class EulerSDK<TVaultEntity extends IVaultEntity = VaultEntity> {
this.feeFlowService = options.feeFlowService;
this.reulLockService = options.reulLockService;
this.positionMigrationService = options.positionMigrationService;
this.activityService =
options.activityService ??
new ActivityService(
new UnavailableActivityAdapter("source-not-configured"),
);
this.plugins = options.plugins ?? [];
}

Expand Down
Loading