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
14 changes: 11 additions & 3 deletions packages/client/src/actions/hubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
} from '@aave/graphql-next';
import type { ResultAsync } from '@aave/types-next';
import type { AaveClient } from '../AaveClient';
import { type CurrencyQueryOptions, DEFAULT_QUERY_OPTIONS } from '../options';
import {
type CurrencyQueryOptions,
DEFAULT_QUERY_OPTIONS,
type RequestPolicyOptions,
} from '../options';

/**
* Fetches a specific hub by address and chain ID.
Expand All @@ -31,9 +35,13 @@ import { type CurrencyQueryOptions, DEFAULT_QUERY_OPTIONS } from '../options';
export function hub(
client: AaveClient,
request: HubRequest,
options: Required<CurrencyQueryOptions> = DEFAULT_QUERY_OPTIONS,
options: CurrencyQueryOptions & RequestPolicyOptions = DEFAULT_QUERY_OPTIONS,
): ResultAsync<Hub | null, UnexpectedError> {
return client.query(HubQuery, { request, ...options });
return client.query(
HubQuery,
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
);
}

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/actions/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { AaveClient } from '../AaveClient';
import {
type CurrencyQueryOptions,
DEFAULT_QUERY_OPTIONS,
type RequestPolicyOptions,
type TimeWindowQueryOptions,
} from '../options';

Expand Down Expand Up @@ -226,9 +227,13 @@ export function userBalances(
export function userHistory(
client: AaveClient,
request: UserHistoryRequest,
options: Required<CurrencyQueryOptions> = DEFAULT_QUERY_OPTIONS,
options: CurrencyQueryOptions & RequestPolicyOptions = DEFAULT_QUERY_OPTIONS,
): ResultAsync<PaginatedUserHistoryResult, UnexpectedError> {
return client.query(UserHistoryQuery, { request, ...options });
return client.query(
UserHistoryQuery,
{ request, currency: options.currency ?? DEFAULT_QUERY_OPTIONS.currency },
options.requestPolicy ?? DEFAULT_QUERY_OPTIONS.requestPolicy,
);
}

/**
Expand Down
134 changes: 134 additions & 0 deletions packages/client/src/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { ReservesRequestFilter } from '@aave/graphql-next';
import {
assertOk,
bigDecimal,
chainId,
evmAddress,
never,
nonNullable,
} from '@aave/types-next';
import { beforeAll, describe, expect, it } from 'vitest';
import { hub, hubs, reserves, supply, userHistory } from './actions';
import {
client,
createNewWallet,
ETHEREUM_FORK_ID,
ETHEREUM_WETH_ADDRESS,
} from './test-utils';
import { sendWith } from './viem';

const user = await createNewWallet();

describe('Given the Aave SDK normalized graph cache', () => {
describe(`When fetching a single 'Hub'`, () => {
it('Then it should leverage cached data whenever possible', async () => {
const primed = await hubs(client, {
query: {
chainIds: [chainId(1)],
},
});
assertOk(primed);

const result = await hub(
client,
{
hub: nonNullable(primed.value[0]).address,
chainId: nonNullable(primed.value[0]).chain.chainId,
},
{
requestPolicy: 'cache-only',
},
);

assertOk(result);
expect(result.value).toEqual(primed.value[0]);
});
});

describe('When fetching user history by tx hash', () => {
beforeAll(async () => {
const setup = await reserves(client, {
query: {
tokens: [
{
chainId: ETHEREUM_FORK_ID,
address: ETHEREUM_WETH_ADDRESS,
},
],
},
filter: ReservesRequestFilter.Supply,
})
.map(
(list) =>
list.find((reserve) => reserve.canSupply) ??
never('No reserve found to supply to for the token'),
)
.andThen((reserve) =>
// supply activity 1
supply(client, {
sender: evmAddress(user.account.address),
reserve: {
chainId: reserve.chain.chainId,
reserveId: reserve.id,
spoke: reserve.spoke.address,
},
amount: {
native: bigDecimal('0.1'),
},
enableCollateral: false, // workaround temporary contracts limitations
})
.andThen(sendWith(user))
.andThen(client.waitForTransaction)
.andThen(() =>
// supply activity 2
supply(client, {
sender: evmAddress(user.account.address),
reserve: {
chainId: reserve.chain.chainId,
reserveId: reserve.id,
spoke: reserve.spoke.address,
},
amount: {
native: bigDecimal('0.1'),
},
enableCollateral: false, // workaround temporary contracts limitations
})
.andThen(sendWith(user))
.andThen(client.waitForTransaction),
),
);

assertOk(setup);
});

it('Then it should leverage cached data whenever possible', async () => {
const primed = await userHistory(client, {
user: evmAddress(user.account.address),
filter: {
chainIds: [ETHEREUM_FORK_ID],
},
});
assertOk(primed);

const result = await userHistory(
client,
{
user: evmAddress(user.account.address),
filter: {
txHash: {
txHash:
primed.value.items[0]?.txHash ?? never('Expected a tx hash'),
chainId: ETHEREUM_FORK_ID,
},
},
},
{
requestPolicy: 'cache-only',
},
);

assertOk(result);
expect(result.value.items[0]).toEqual(primed.value.items[0]);
});
});
});
136 changes: 112 additions & 24 deletions packages/client/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import type {
BorrowActivity,
Chain,
Erc20Token,
Hub,
HubAsset,
LiquidatedActivity,
NativeToken,
RepayActivity,
Reserve,
ReserveInfo,
Spoke,
SupplyActivity,
SwapActivity,
SwapByIntent,
SwapByIntentWithApprovalRequired,
SwapByTransaction,
UserPosition,
WithdrawActivity,
import {
type BorrowActivity,
type Chain,
type Erc20Token,
type Hub,
type HubAsset,
type HubQuery,
isTxHashInputVariant,
type LiquidatedActivity,
type NativeToken,
type RepayActivity,
type Reserve,
type ReserveInfo,
type Spoke,
type SupplyActivity,
type SwapActivity,
type SwapByIntent,
type SwapByIntentWithApprovalRequired,
type SwapByTransaction,
type UserHistoryQuery,
type UserPosition,
type VariablesOf,
type WithdrawActivity,
} from '@aave/graphql-next';
import introspectedSchema from '@aave/graphql-next/schema';
import type { DateTime, TxHash } from '@aave/types-next';
import {
cacheExchange,
type Resolver,
Expand Down Expand Up @@ -46,17 +51,100 @@ export const exchange = cacheExchange({
// value: transformToBigInt,
// nonce: transformToBigInt,
// },

Query: {
hub: (_, { request }: VariablesOf<typeof HubQuery>) => {
return {
__typename: 'Hub',
address: request.hub,
chain: {
__typename: 'Chain',
chainId: request.chainId,
},
assets: [],
};
},

userHistory: (
_parent,
args: VariablesOf<typeof UserHistoryQuery>,
cache,
) => {
// Bail out if not a txHash filter lookup
if (!isTxHashInputVariant(args.request.filter)) {
return cache.resolve('Query', 'userHistory', args);
}

const { txHash, chainId } = args.request.filter.txHash;

// Collect all cached pages for Query.userHistory
const matches = cache
.inspectFields('Query')
.filter((f) => f.fieldName === 'userHistory')
.reduce((set, f) => {
const pageRef = cache.resolve('Query', f.fieldKey) as string | null;
if (!pageRef) return set;

const itemRefs = cache.resolve(pageRef, 'items') as string[] | null;
if (!itemRefs) return set;

for (const ref of itemRefs) {
set.add(ref);
}
return set;
}, new Set<string>())
.values()
.toArray()
.filter((ref) => {
const itemTxHash = cache.resolve(ref, 'txHash') as TxHash;
if (itemTxHash !== txHash) return false;

// Verify chainId if spoke.chain.chainId exists
const spokeRef = cache.resolve(ref, 'spoke') as string | null;
if (spokeRef) {
const chainRef = cache.resolve(spokeRef, 'chain') as
| string
| null;
const itemChainId = chainRef
? (cache.resolve(chainRef, 'chainId') as number | undefined)
: undefined;
if (typeof itemChainId === 'number') {
return itemChainId === chainId;
}
}
return true;
})
.sort((a, b) => {
const ta = cache.resolve(a, 'id') as DateTime;
const tb = cache.resolve(b, 'id') as DateTime;
return tb.localeCompare(ta); // desc
});

if (matches.length === 0) return undefined;

return {
__typename: 'PaginatedUserHistoryResult',
items: matches,
pageInfo: {
__typename: 'PaginatedResultInfo',
prev: null,
next: null,
},
};
},
},
},
keys: {
// Entitied with composite key
Hub: (data: Hub) => `Hub:${data.address}/chain:${data.chain.chainId}`,
Hub: (data: Hub) => `address=${data.address},chain=${data.chain.chainId}`,
HubAsset: (data: HubAsset) =>
`HubAsset:${data.assetId}/hub:${data.hub.address}/chain:${data.hub.chain.chainId}`,
`assetId=${data.assetId},hub=${data.hub.address},chain=${data.hub.chain.chainId}`,
Reserve: (data: Reserve) =>
`Reserve:${data.id}/spoke:${data.spoke.address}/chain:${data.chain.chainId}`,
`reserveId=${data.id},spoke=${data.spoke.address},chain=${data.chain.chainId}`,
ReserveInfo: (data: ReserveInfo) =>
`ReserveInfo:${data.id}/spoke:${data.spoke.address}/chain:${data.chain.chainId}`,
Spoke: (data: Spoke) => `Spoke:${data.address}/chain:${data.chain.chainId}`,
`reserveId=${data.id},spoke=${data.spoke.address},chain=${data.chain.chainId}`,
Spoke: (data: Spoke) =>
`address=${data.address},chain=${data.chain.chainId}`,

// Entities with id field as key
BorrowActivity: (data: BorrowActivity) => data.id,
Expand Down
12 changes: 12 additions & 0 deletions packages/client/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Currency, TimeWindow } from '@aave/graphql-next';
import type { RequestPolicy } from '@urql/core';

export type CurrencyQueryOptions = {
/**
Expand All @@ -18,7 +19,18 @@ export type TimeWindowQueryOptions = {
timeWindow?: TimeWindow;
};

export type RequestPolicyOptions = {
/**
* The request policy to use.
*
* @internal This is used for testing purposes and could be changed without notice.
* @defaultValue `cache-and-network`
*/
requestPolicy?: RequestPolicy;
};

export const DEFAULT_QUERY_OPTIONS = {
currency: Currency.Usd,
timeWindow: TimeWindow.LastDay,
requestPolicy: 'cache-and-network',
} as const;
2 changes: 1 addition & 1 deletion packages/graphql/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type {
} from './enums';
import type { introspection } from './graphql-env';

export type { FragmentOf } from 'gql.tada';
export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';

export const graphql = initGraphQLTada<{
disableMasking: true;
Expand Down
10 changes: 10 additions & 0 deletions packages/graphql/src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ export type ReserveErc20AmountInputWithPermit = ReturnType<
typeof graphql.scalar<'ReserveErc20AmountInputWithPermit'>
>;
export type TxHashInput = ReturnType<typeof graphql.scalar<'TxHashInput'>>;

/**
* @internal
*/
export function isTxHashInputVariant<T>(
input: T,
): input is T & { txHash: TxHashInput } {
return isObject(input) && 'txHash' in input && input.txHash != null;
}

export type UserSpokeInput = ReturnType<
typeof graphql.scalar<'UserSpokeInput'>
>;
Expand Down