Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0bddedd
feat(earn): add a positions client for the Blend account endpoint
CassioMG Aug 26, 2026
726cc9e
chore(earn): log raw Blend responses while building positions
CassioMG Aug 26, 2026
13b1cf7
feat(earn): cache account positions alongside balances and collectibles
CassioMG Aug 26, 2026
66d6b3e
fix(earn): read the positions cache at call time instead of subscribing
CassioMG Aug 26, 2026
916f7b0
feat(home): fetch positions with the rest of the account data
CassioMG Aug 26, 2026
1a688e2
fix(earn): bypass the positions cache on refresh and account switch
CassioMG Aug 26, 2026
d3979a4
feat(home): add the Positions tab
CassioMG Aug 26, 2026
dd0e267
fix(home): hide the floating add button on the Positions tab
CassioMG Aug 26, 2026
f77a1f4
feat(home): list supplied tokens on the Positions tab
CassioMG Aug 26, 2026
5afc057
fix(home): keep a flat-zero interest gain from reading as colored
CassioMG Aug 26, 2026
6fbf8a5
test(home): cover the interest-gain null/zero/positive boundary
CassioMG Aug 26, 2026
2e8aaac
feat(home): project potential earnings on the empty Positions tab
CassioMG Aug 26, 2026
a398cf8
docs(home): correct the empty-state loader's stale Task 7 reference
CassioMG Aug 26, 2026
3f29a52
feat(earn): derive a pool position's summary figures
CassioMG Aug 26, 2026
77e88ee
fix(earn): keep deposit usd null when interest usd is unknown
CassioMG Aug 26, 2026
5f97c56
feat(earn): give the pool sheet a Your position tab
CassioMG Aug 26, 2026
abd6f1c
docs(earn): correct the pool sheet's green-shade provenance comments
CassioMG Aug 26, 2026
e3f4551
feat(earn): open the pool sheet from a position row
CassioMG Aug 26, 2026
35dc3db
feat(earn): show the position tab during a deposit
CassioMG Aug 26, 2026
8fee5d1
feat(earn): attribute deposits to where the flow started
CassioMG Aug 26, 2026
f5792f5
test(earn): cover the Positions tab end to end
CassioMG Aug 26, 2026
0f78856
fix(earn): surface fetch failures on the Positions tab and unblock th…
CassioMG Aug 26, 2026
aded712
fix(earn): stop the pool sheet showing a negative or another asset's …
CassioMG Aug 26, 2026
33895fc
refactor(earn): extract headlineApy to one shared implementation
CassioMG Aug 26, 2026
57396dc
fix(home): stop the earnOptions dispatch reverting the Blockaid-scann…
CassioMG Aug 26, 2026
2317c20
refactor(earn): use NO_FIAT_VALUE for the positions placeholder dashes
CassioMG Aug 26, 2026
edb6353
feat(earn): summarise a pool position's totals
CassioMG Aug 26, 2026
9348d37
docs(earn): stop describing the deleted pool-scope path in YourPosition
CassioMG Aug 26, 2026
811f9f4
feat(home): add a pool card for the Positions tab
CassioMG Aug 26, 2026
f6a8b95
fix(home): stop the pool card's global class colliding with earn's
CassioMG Aug 26, 2026
ac152aa
feat(home): list pools on the Positions tab
CassioMG Aug 26, 2026
9dd62e1
fix(home): translate the My position title and cover PositionRow dire…
CassioMG Aug 26, 2026
6ba27d4
feat(home): show a pool's totals and supplied tokens
CassioMG Aug 26, 2026
cc5b145
feat(earn): open the pool sheet as overview only from About pool
CassioMG Aug 26, 2026
fb14e93
feat(earn): gate every deposit entry point behind the earn flag
CassioMG Aug 26, 2026
0580226
feat(earn): track the pool-first positions navigation
CassioMG Aug 26, 2026
596a0be
test(earn): cover the pool-first positions navigation end to end
CassioMG Aug 26, 2026
56387f6
test(earn): verify the deposit CTA actually prefills the amount screen
CassioMG Aug 26, 2026
2676c9a
fix(earn): stop formatAccountUsd flooring exact-cent values one cent low
CassioMG Aug 26, 2026
9621772
fix(earn): total My position from supplied, not net of borrow
CassioMG Aug 26, 2026
4b0b34b
test(earn): stop the Deposit-CTA test depending on the flag's default
CassioMG Aug 26, 2026
b5ef815
fix(earn): give the pool-details tab strip a tablist role
CassioMG Aug 26, 2026
0a44ca2
refactor(earn): drop MyPosition's unused pool prop
CassioMG Aug 26, 2026
0cb80fc
fix(earn): stop a refresh tick reopening My position on its own
CassioMG Aug 26, 2026
e430f7d
docs(earn): drop internal plan-artifact references from comments
CassioMG Aug 26, 2026
6249f9d
test(earn): make the earn_deposit flag stub actually reach the popup
CassioMG Aug 27, 2026
865081b
style(home): match the positions tab to the design
CassioMG Aug 27, 2026
a514054
style(home): match the my position sheet to the design
CassioMG Aug 27, 2026
60a705a
style(earn): match the pool details sheet to the design
CassioMG Aug 27, 2026
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
202 changes: 202 additions & 0 deletions @shared/api/helpers/__tests__/blend.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
getBlendEarnOptions,
getBlendPools,
getBlendPositions,
getBlendSuppliedTokens,
} from "../blend";
import { sendMessageToBackground } from "../extensionMessaging";
Expand Down Expand Up @@ -385,3 +386,204 @@ describe("getBlendSuppliedTokens", () => {
await expect(call()).rejects.toThrow();
});
});

describe("getBlendPositions", () => {
const call = () =>
getBlendPositions({ publicKey: PUBLIC_KEY, networkDetails });

it("POSTs the address with the network in the path", async () => {
// The query must live in `path` — authedFetch signs pathname + search.
mockedSend.mockResolvedValue({
status: 200,
body: { data: [{ address: PUBLIC_KEY, positions: [], backstop: [] }] },
});

await call();

expect(mockedSend).toHaveBeenCalledWith({
type: SERVICE_TYPES.FETCH_BACKEND_V2,
activePublicKey: null,
method: "POST",
path: "/accounts/positions?network=PUBLIC",
body: JSON.stringify({ addresses: [PUBLIC_KEY] }),
});
});

it("maps snake_case wire fields to camelCase", async () => {
mockedSend.mockResolvedValue({
status: 200,
body: {
data: [
{
address: PUBLIC_KEY,
total_value_usd: 500.12,
net_apy: 0.1694,
positions: [
{
protocol: "blend",
id: POOL_ID,
name: "Fixed Pool v2",
net_usd: 500.12,
supplied_usd: 500.12,
borrowed_usd: 0,
net_apy: 0.1694,
blend: {
supply: [
{
asset_id: USDC_SAC,
symbol: "USDC",
name: "USDC:GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM",
decimals: 7,
supplied_tokens: "0",
collateral_tokens: "5001223000",
total_tokens: "5001223000",
usd_value: 500.12,
apy: 0.1694,
emissions_apr: 0,
interest_earned: "1234000",
interest_earned_usd: 0.12,
claimable_blnd: "0",
claimable_usd: null,
price_usd: 1.0,
},
],
borrow: [],
},
},
],
backstop: [],
},
],
},
});

const result = await call();

expect(result.totalValueUsd).toBe(500.12);
expect(result.positions[0].id).toBe(POOL_ID);
expect(result.positions[0].netUsd).toBe(500.12);
const supply = result.positions[0].blend!.supply[0];
expect(supply.assetId).toBe(USDC_SAC);
expect(supply.totalTokens).toBe("5001223000");
expect(supply.interestEarnedUsd).toBe(0.12);
expect(supply.emissionsApr).toBe(0);
});

it("preserves null as unavailable rather than coalescing to zero", async () => {
mockedSend.mockResolvedValue({
status: 200,
body: {
data: [
{
address: PUBLIC_KEY,
total_value_usd: null,
net_apy: null,
positions: [
{
protocol: "blend",
id: POOL_ID,
name: null,
net_usd: null,
supplied_usd: null,
borrowed_usd: null,
net_apy: null,
blend: { supply: [], borrow: [] },
},
],
backstop: [],
},
],
},
});

const result = await call();

expect(result.totalValueUsd).toBeNull();
expect(result.positions[0].netApy).toBeNull();
});

it("returns an empty result when the batch carries no entry for the address", async () => {
// The contract guarantees one entry per requested address; an empty result
// is the right degradation if that ever slips, not a throw.
mockedSend.mockResolvedValue({ status: 200, body: { data: [] } });

const result = await call();

expect(result).toEqual({
address: PUBLIC_KEY,
totalValueUsd: null,
netApy: null,
positions: [],
backstop: [],
});
});

it("tolerates a pool row carrying no blend detail", async () => {
mockedSend.mockResolvedValue({
status: 200,
body: {
data: [
{
address: PUBLIC_KEY,
positions: [{ protocol: "blend", id: POOL_ID }],
backstop: [],
},
],
},
});

const result = await call();

expect(result.positions[0].blend).toBeUndefined();
});

it("maps backstop rows including their q4w queue", async () => {
mockedSend.mockResolvedValue({
status: 200,
body: {
data: [
{
address: PUBLIC_KEY,
positions: [],
backstop: [
{
pool_id: POOL_ID,
pool_name: "Fixed Pool v2",
shares: "100",
lp_tokens: "120",
usd_value: 12.5,
claimable_blnd: "3",
claimable_usd: null,
q4w: [
{
amount: "20",
lp_tokens: "20",
usd_value: 2.1,
expiration: 1790000000,
},
],
},
],
},
],
},
});

const result = await call();

expect(result.backstop[0].poolId).toBe(POOL_ID);
expect(result.backstop[0].q4w[0].expiration).toBe(1790000000);
});

it("throws on a non-200", async () => {
mockedSend.mockResolvedValue({ status: 500, body: {} });

await expect(call()).rejects.toThrow();
});

it("throws when a 200 carries no data payload", async () => {
mockedSend.mockResolvedValue({ status: 200, body: {} });

await expect(call()).rejects.toThrow();
});
});
Loading
Loading