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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ coverage/
.nyc_output/
playwright-report/
test-results/
.vitest/
*.lcov
*.tsbuildinfo
next-env.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pinned to effect@4.0.0-rc.112 (matches widget catalog effect version).
# Pinned to effect@4.0.0-rc.115 (matches widget catalog effect version).
[submodule ".repos/effect"]
path = .repos/effect
url = https://github.com/Effect-TS/effect.git
2 changes: 1 addition & 1 deletion .repos/effect
Submodule effect updated 1300 files
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ source of truth for behavior.
| Ownership, dependency direction, and Module interfaces | `packages/widget/ARCHITECTURE.md` |
| Project vocabulary | `CONTEXT.md` |
| A decision that constrains the change | The relevant current file in `docs/adr/` |
| Effect APIs | Run `git submodule update --init .repos/effect` (pinned to the same release as the catalog `effect` version, currently `effect@4.0.0-rc.112`), then read `.repos/effect/LLMS.md`, then the exact upstream source or tests |
| Effect APIs | Run `git submodule update --init .repos/effect` (pinned to the same release as the catalog `effect` version), then read `.repos/effect/LLMS.md`, then the exact upstream source or tests |
| Local issues, domain docs, and triage | `docs/agents/` |
| Publishing | `docs/releases.md` |

Expand All @@ -26,6 +26,7 @@ Run pnpm through the version pinned by mise: `mise exec -- pnpm ...`.
- `pnpm --filter @stakekit/widget test:unit` — Node tests.
- `pnpm --filter @stakekit/widget test:dom` — jsdom tests.
- `pnpm --filter @stakekit/widget test:browser` — Chromium tests.
- `pnpm --filter @stakekit/widget test:browser:diagnostic` — Chromium tests with trace replay in `packages/widget/.vitest/index.html`; serve that directory over HTTP to view the report.
- `pnpm check-hygiene` — dependency-cruiser and Knip; run after changing the import graph.
- `pnpm lint` — all workspaces plus the root ast-grep rules.
- `pnpm check` — lint, hygiene, tests, and builds.
Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"!**/.next",
"!**/next",
"!**/.turbo",
"!**/.vitest",
"!**/node_modules",
"!**/dist",
"!**/.next",
Expand Down
3 changes: 2 additions & 1 deletion packages/widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"test:dom": "vitest -c vite/vitest.config.ts --project dom run",
"test:dom:watch": "vitest -c vite/vitest.config.ts --project dom",
"test:browser": "vitest -c vite/vitest.config.ts --project browser run",
"test:browser:diagnostic": "vitest -c vite/vitest.config.ts --project browser run --browser.traceView --reporter=default --reporter=html",
"test:changed": "vitest -c vite/vitest.config.ts run --project unit --project dom --changed",
"test:changed:all": "vitest -c vite/vitest.config.ts run --changed",
"test:changed:unit": "vitest -c vite/vitest.config.ts run --project unit --changed",
Expand Down Expand Up @@ -105,7 +106,6 @@
"@effect/platform-browser": "catalog:",
"@effect/platform-node": "catalog:",
"@effect/vitest": "catalog:",
"@faker-js/faker": "catalog:",
"@ledgerhq/wallet-api-client": "catalog:",
"@ledgerhq/wallet-api-core": "catalog:",
"@luno-kit/core": "catalog:",
Expand Down Expand Up @@ -143,6 +143,7 @@
"@vanilla-extract/vite-plugin": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/browser-playwright": "catalog:",
"@vitest/ui": "catalog:",
"@wallet-standard/app": "catalog:",
"@wallet-standard/base": "catalog:",
"autoprefixer": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/domain/borrow/ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const AddressedTokenId = Schema.TemplateLiteral([
Schema.Literal("address:"),
Schema.NonEmptyString,
Schema.Literal(":"),
TokenAddress,
CanonicalTokenAddress,
]);
const NativeTokenId = Schema.TemplateLiteral([
Schema.Literal("native:"),
Expand Down
4 changes: 2 additions & 2 deletions packages/widget/src/domain/decoding/response-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const TolerantTopLevelArray = <
) =>
Schema.Array(Schema.Unknown).pipe(
Schema.decodeTo(Schema.Array(Schema.toType(item)), {
decode: SchemaGetter.transformOrFail((inputs) =>
decode: SchemaGetter.transformEffect((inputs) =>
Effect.forEach(inputs, (input, index) => {
const result = Schema.decodeUnknownResult(item)(input);

Expand Down Expand Up @@ -74,7 +74,7 @@ export const TolerantTopLevelRecord = <

return Schema.Record(Schema.String, Schema.Unknown).pipe(
Schema.decodeTo(target, {
decode: SchemaGetter.transformOrFail((input) =>
decode: SchemaGetter.transformEffect((input) =>
Effect.forEach(Object.entries(input), ([rawKey, rawValue]) => {
const decodedKey = Schema.decodeResult(key)(rawKey);
const decodedValue = Schema.decodeUnknownResult(value)(rawValue);
Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/domain/earn/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ const TronResourceArgument = decodeApiArgument(
"tronResource",
"enum",
TronResourceArgumentDomain,
SchemaGetter.transformOrFail<
SchemaGetter.transformEffect<
typeof TronResourceArgumentDomain.Encoded,
ApiArgumentField
>((field, options) =>
Expand Down
4 changes: 2 additions & 2 deletions packages/widget/src/domain/finance/scalars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const TolerantOptionalUtcDateTimeFromString = (
) =>
Schema.Unknown.pipe(
Schema.decodeTo(Schema.UndefinedOr(Schema.DateTimeUtc), {
decode: SchemaGetter.transformOrFail((input) => {
decode: SchemaGetter.transformEffect((input) => {
if (input === undefined) {
// Effect.void widens the success type to void, which is incompatible
// with the schema's precise DateTime.Utc | undefined output.
Expand All @@ -112,7 +112,7 @@ export const TolerantNullableUtcDateTimeFromString = (
) =>
Schema.Unknown.pipe(
Schema.decodeTo(Schema.NullOr(Schema.DateTimeUtc), {
decode: SchemaGetter.transformOrFail((input) =>
decode: SchemaGetter.transformEffect((input) =>
input === null
? Effect.succeed(null)
: decodeOptionalUtcDateTime(input, options).pipe(
Expand Down
2 changes: 2 additions & 0 deletions packages/widget/tests/borrow/domain/risk-position.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ describe("BorrowPositions", () => {
})
);
expect(() => Schema.decodeUnknownSync(TokenId)("ETH::native")).toThrow();
expect(() => Schema.decodeSync(TokenId)("address:ETH:")).toThrow();
expect(() => Schema.decodeSync(TokenId)("native:")).toThrow();
});

it("decodes underwater current LTV without hiding the account snapshot", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ describe("Position source yield details", () => {
await userEvent.click(app.getByTestId("position-source-details-trigger"));

await expect.element(app.getByText("Fees")).toBeInTheDocument();
await expect.element(app.getByText("2%")).toBeInTheDocument();
await expect
.element(app.getByText("2%", { exact: false }))
.toBeInTheDocument();
await expect
.element(app.getByText("Price per share"))
.not.toBeInTheDocument();
Expand Down
31 changes: 23 additions & 8 deletions packages/widget/tests/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { DateTime, Schema } from "effect";
import { DateTime, Effect, Random, Schema } from "effect";
import { Arbitrary } from "effect/unstable/arbitrary";
import { ActionTransaction, YieldAction } from "../../src/domain/action/models";
import type {
EarnBalance,
Expand All @@ -20,7 +20,22 @@ import type { YieldDto as YieldApiYieldDto } from "../generated/yield-api-types"

type ValidatorDto = typeof EarnValidator.Encoded;
type YieldApiProviderDto = typeof EarnProvider.Encoded;
const apyFaker = () => faker.number.float({ min: 0, max: 0.05 });
// ast-grep-ignore: no-run-effect-in-test -- synchronous fixture helper for random APY values
const apyFaker = () => Effect.runSync(Random.nextBetween(0, 0.05));

const ethereumAddressArbitrary = Arbitrary.schema(
Schema.String.check(Schema.isPattern(/^0x[a-f0-9]{40}$/))
);
const ethereumAddressFaker = () =>
// ast-grep-ignore: no-run-effect-in-test -- synchronous fixture helper for generated addresses
Effect.runSync(
Arbitrary.sampleEffect(ethereumAddressArbitrary, { count: 1 })
)[0]!;

const uuidArbitrary = Arbitrary.schema(Schema.String.check(Schema.isUUID(4)));
const uuidFaker = () =>
// ast-grep-ignore: no-run-effect-in-test -- synchronous fixture helper for generated UUIDs
Effect.runSync(Arbitrary.sampleEffect(uuidArbitrary, { count: 1 }))[0]!;

type ExactDecimalFixtureInput = Parameters<typeof exactDecimal>[0];
type YieldRewardComponent = YieldRewardRate["components"][number];
Expand Down Expand Up @@ -193,7 +208,7 @@ export const yieldApiValidatorFixture = (
const { rewardRate, ...rest } = overrides ?? {};

return {
address: faker.finance.ethereumAddress(),
address: ethereumAddressFaker(),
commission: 0,
logoURI: "https://assets.stakek.it/validators/default.png",
name: "StakeKit Validator",
Expand All @@ -210,7 +225,7 @@ export const yieldBalanceFixture = (
const token = overrides?.token ?? yieldApiYieldFixture().token;

return {
address: faker.finance.ethereumAddress(),
address: ethereumAddressFaker(),
type: "active",
amount: "1",
amountRaw: "1000000000000000000",
Expand Down Expand Up @@ -298,7 +313,7 @@ export const yieldApiValidatorsFixture = (
export const yieldApiTransactionDtoFixture = (
overrides?: Partial<typeof ActionTransaction.Encoded>
): typeof ActionTransaction.Encoded => ({
id: faker.string.uuid(),
id: uuidFaker(),
title: "Stake",
network: "ethereum",
status: "CREATED",
Expand Down Expand Up @@ -340,11 +355,11 @@ export const yieldApiActionDtoFixture = (
const intent = overrides?.intent ?? getYieldActionIntent(type);

return {
id: faker.string.uuid(),
id: uuidFaker(),
intent,
type,
yieldId: "ethereum-eth-native-staking",
address: faker.finance.ethereumAddress(),
address: ethereumAddressFaker(),
amount: null,
amountRaw: null,
amountUsd: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ describe("Borrow position details", () => {
await expect.element(app.getByText("Total supplied")).toBeInTheDocument();
await expect.element(app.getByText("Net worth")).toBeInTheDocument();
await expect.element(app.getByText("WETH/USDC")).toBeInTheDocument();
await expect.element(app.getByText("Aave V3")).toBeInTheDocument();
await expect
.element(app.getByText("Aave V3", { exact: false }))
.toBeInTheDocument();

await userEvent.click(app.getByText("WETH/USDC"));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ describe("ProviderSelectionCard", () => {

const app = await renderProviderSelectionCard();

await expect.element(app.getByText("Inactive")).toBeInTheDocument();
await expect
.element(app.getByText("Inactive", { exact: false }))
.toBeInTheDocument();
});

it("keeps the selector available from the multi-validator dashboard card", async () => {
Expand Down
22 changes: 12 additions & 10 deletions packages/widget/tests/resources/activity-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,21 @@ describe("Activity History resource", () => {
});

it("shares normalized requests and loads one semantic batch per Pull", async () => {
const listActivity = vi.fn((request: { readonly offset: number }) =>
const mockActivityPage = (actionId: string, offset: number) =>
Effect.succeed({
items: [
yieldApiActionFixture({
id: request.offset === 0 ? "action-a" : "action-b",
yieldId,
}),
],
items: [yieldApiActionFixture({ id: actionId, yieldId })],
limit: 1,
offset: request.offset,
offset,
total: 2,
})
);
});

const listActivity =
vi.fn<YieldResourceSource["Service"]["listActivity"]>();
vi.when(listActivity, { onUnmatched: "throw" })
.calledWith(expect.objectContaining({ offset: 0 }))
.thenReturn(mockActivityPage("action-a", 0))
.calledWith(expect.objectContaining({ offset: 1 }))
.thenReturn(mockActivityPage("action-b", 1));
const registry = makeRegistry(listActivity);
const firstPull = activityHistoryPullAtom(makeKey());
const equivalentPull = activityHistoryPullAtom(makeKey());
Expand Down
37 changes: 28 additions & 9 deletions packages/widget/tests/resources/earn-token-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,38 @@ describe("Earn Token Catalog", () => {
});

it("omits failed dashboard categories when another category is usable", () => {
const getTokenOptions = vi.fn((request: EarnTokenCatalogRequest) => {
if (request.yieldTypes?.includes("staking")) {
return Effect.fail(
const ethereumYieldRequest = (yieldTypes: ReadonlyArray<string>) =>
expect.objectContaining({
enter: true,
network: "ethereum",
yieldTypes,
});

const getTokenOptions = vi.fn<Parameters<typeof makeRegistry>[0]>();
vi.when(getTokenOptions, { onUnmatched: "throw" })
.calledWith(
ethereumYieldRequest(["staking", "restaking", "liquid_staking"])
)
.thenReturn(
Effect.fail(
new ApiRequestError({
cause: new Error("offline"),
operation: "legacy-token-options",
})
);
}
return Effect.succeed(
request.yieldTypes?.includes("lending") ? [tokenOption] : []
);
});
)
)
.calledWith(
ethereumYieldRequest([
"lending",
"vault",
"fixed_yield",
"concentrated_liquidity_pool",
"liquidity_pool",
])
)
.thenReturn(Effect.succeed([tokenOption]))
.calledWith(ethereumYieldRequest(["real_world_asset"]))
.thenReturn(Effect.succeed([]));
const registry = makeRegistry(getTokenOptions);
const result = registry.get(
availableYieldCategoriesAtom(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ describe("Select opportunity", () => {
await clickOpportunity("ethereum-eth-reth-staking");

await expect
.element(app.getByText("You'll receive").first())
.element(app.getByText("You'll receive", { exact: false }).first())
.toBeInTheDocument();
await expect.element(app.getByText("rETH").first()).toBeInTheDocument();
await expect
Expand All @@ -295,7 +295,7 @@ describe("Select opportunity", () => {
await clickOpportunity("ethereum-eth-lido-staking");

await expect
.element(app.getByText("You'll receive").first())
.element(app.getByText("You'll receive", { exact: false }).first())
.toBeInTheDocument();
await expect.element(app.getByText("stETH").first()).toBeInTheDocument();
await expect.element(app.getByText("Rated by Credora")).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe("Staking flow", () => {
const rewardTokenDetails = getYieldSummaryRewardToken(yieldOp)!;

await expect
.element(app.getByText(`You'll receive`).first())
.element(app.getByText("You'll receive", { exact: false }).first())
.toBeInTheDocument();
await expect
.element(
Expand Down Expand Up @@ -116,7 +116,9 @@ describe("Staking flow", () => {
await expect
.element(app.getByText(yieldOp.token.symbol).first())
.toBeInTheDocument();
await expect.element(app.getByText("& earn").first()).toBeInTheDocument();
await expect
.element(app.getByText("& earn", { exact: false }).first())
.toBeInTheDocument();
await expect.element(app.getByText("5.08%").first()).toBeInTheDocument();

await expect
Expand Down
Loading
Loading