@ember-finance/sdk 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/src/placements/adapters/alphalend.d.ts +1 -0
  2. package/dist/src/placements/adapters/alphalend.js +58 -0
  3. package/dist/src/placements/adapters/bluefin-spot.d.ts +1 -0
  4. package/dist/src/placements/adapters/bluefin-spot.js +78 -0
  5. package/dist/src/placements/adapters/current.d.ts +1 -0
  6. package/dist/src/placements/adapters/current.js +70 -0
  7. package/dist/src/placements/adapters/evm-protocols.d.ts +1 -0
  8. package/dist/src/placements/adapters/evm-protocols.js +8 -0
  9. package/dist/src/placements/adapters/factory.d.ts +15 -0
  10. package/dist/src/placements/adapters/factory.js +62 -0
  11. package/dist/src/placements/adapters/index.d.ts +14 -0
  12. package/dist/src/placements/adapters/index.js +25 -0
  13. package/dist/src/placements/adapters/navi.d.ts +1 -0
  14. package/dist/src/placements/adapters/navi.js +53 -0
  15. package/dist/src/placements/adapters/pendle.d.ts +9 -0
  16. package/dist/src/placements/adapters/pendle.js +108 -0
  17. package/dist/src/placements/adapters/royco.d.ts +2 -0
  18. package/dist/src/placements/adapters/royco.js +49 -0
  19. package/dist/src/placements/adapters/suilend.d.ts +1 -0
  20. package/dist/src/placements/adapters/suilend.js +57 -0
  21. package/dist/src/placements/batch.d.ts +70 -0
  22. package/dist/src/placements/batch.js +197 -0
  23. package/dist/src/placements/client.d.ts +17 -0
  24. package/dist/src/placements/client.js +194 -0
  25. package/dist/src/placements/evm.d.ts +29 -0
  26. package/dist/src/placements/evm.js +280 -0
  27. package/dist/src/placements/index.d.ts +9 -0
  28. package/dist/src/placements/index.js +9 -0
  29. package/dist/src/placements/pointers.d.ts +24 -0
  30. package/dist/src/placements/pointers.js +126 -0
  31. package/dist/src/placements/registry.d.ts +11 -0
  32. package/dist/src/placements/registry.js +19 -0
  33. package/dist/src/placements/sui.d.ts +47 -0
  34. package/dist/src/placements/sui.js +302 -0
  35. package/dist/src/placements/types.d.ts +147 -0
  36. package/dist/src/placements/types.js +1 -0
  37. package/dist/src/placements/vaults.d.ts +15 -0
  38. package/dist/src/placements/vaults.js +97 -0
  39. package/package.json +6 -1
@@ -0,0 +1 @@
1
+ export declare const alphalend: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,58 @@
1
+ import { coinTypesMatch, tableId, toBigInt, typeArguments } from "../sui.js";
2
+ import { createSuiVenueAdapter } from "./factory.js";
3
+ /** AlphaLend scales its xToken exchange rate by 1e18. */
4
+ const RATIO_SCALE = 10n ** 18n;
5
+ function ratioOf(field) {
6
+ return toBigInt(field?.value ?? field);
7
+ }
8
+ export const alphalend = createSuiVenueAdapter({
9
+ id: "alphalend",
10
+ async read(ref, account, reader) {
11
+ // AlphaLend. An account holds a `position::PositionCap` pointing at a position
12
+ // object, whose dynamic fields carry one entry per market it is in.
13
+ //
14
+ // Balances are denominated in xTokens, an interest-bearing share, so they are
15
+ // converted through the market's `xtoken_ratio` to the underlying coin the
16
+ // caller asked about.
17
+ const protocols = await reader.objects([ref.venue]);
18
+ const protocol = protocols[ref.venue];
19
+ if (!protocol) {
20
+ throw new Error(`alphalend: lending protocol ${ref.venue} not found on chain`);
21
+ }
22
+ const capType = ref.shareToken ?? `${protocol.type.split("::")[0]}::position::PositionCap`;
23
+ const caps = await reader.ownedObjects(account, capType);
24
+ if (caps.length === 0)
25
+ return undefined;
26
+ const markets = tableId(protocol.json.markets);
27
+ if (!markets)
28
+ throw new Error(`alphalend: no markets table on ${ref.venue}`);
29
+ // Which market holds our coin, and what an xToken is worth there.
30
+ const entries = await reader.dynamicFields(markets);
31
+ const market = entries.find(entry => coinTypesMatch(entry.value?.coin_type ?? "", ref.coin));
32
+ if (!market)
33
+ return undefined;
34
+ const marketValue = market.value;
35
+ const marketId = String(marketValue.market_id);
36
+ const ratio = ratioOf(marketValue.xtoken_ratio);
37
+ let xTokens = 0n;
38
+ for (const cap of caps) {
39
+ const positionId = cap.json.position_id;
40
+ if (!positionId)
41
+ continue;
42
+ const held = await reader.dynamicFields(positionId);
43
+ for (const entry of held) {
44
+ if (String(entry.name) !== marketId)
45
+ continue;
46
+ // The entry's type argument names the coin, so a position in a
47
+ // different market of the same id cannot be mistaken for this one.
48
+ const [entryCoin] = typeArguments(entry.valueType);
49
+ if (entryCoin && !coinTypesMatch(entryCoin, ref.coin))
50
+ continue;
51
+ xTokens += toBigInt(entry.value?.x_token);
52
+ }
53
+ }
54
+ if (xTokens === 0n)
55
+ return undefined;
56
+ return (xTokens * ratio) / RATIO_SCALE;
57
+ }
58
+ });
@@ -0,0 +1 @@
1
+ export declare const bluefinSpot: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,78 @@
1
+ import { coinTypesMatch, toBigInt, typeArguments } from "../sui.js";
2
+ import { createSuiVenueAdapter } from "./factory.js";
3
+ /** Sui wraps a signed tick as `{ bits }`, two's complement in a u32. */
4
+ function tickOf(field) {
5
+ const bits = Number(field?.bits ?? 0);
6
+ return bits >= 0x80000000 ? bits - 0x100000000 : bits;
7
+ }
8
+ const Q64 = 2n ** 64n;
9
+ /**
10
+ * √(1.0001^tick) as a Q64 fixed point.
11
+ *
12
+ * Computed in floating point rather than by the usual chain of magic constants.
13
+ * A double carries ~15 significant digits against Q64's ~19, so the result can
14
+ * be off by roughly 1e-16 relative — orders of magnitude below the dust these
15
+ * amounts are displayed to, and far less error-prone than transcribing the
16
+ * bit-manipulation table.
17
+ */
18
+ function sqrtRatioAtTick(tick) {
19
+ return BigInt(Math.floor(Math.pow(1.0001, tick / 2) * Number(Q64)));
20
+ }
21
+ /**
22
+ * The token amounts a concentrated-liquidity position is currently worth.
23
+ *
24
+ * A position holds liquidity over a tick range, not amounts, so what it is
25
+ * worth depends on where the pool's price sits relative to that range: entirely
26
+ * in coin A above it, entirely in coin B below it, and split while inside.
27
+ */
28
+ function positionAmounts(liquidity, sqrtLower, sqrtUpper, sqrtPrice) {
29
+ if (liquidity === 0n || sqrtLower >= sqrtUpper)
30
+ return { amountA: 0n, amountB: 0n };
31
+ const clamped = sqrtPrice < sqrtLower ? sqrtLower : sqrtPrice > sqrtUpper ? sqrtUpper : sqrtPrice;
32
+ // amountA = L * (√upper - √p) / (√p * √upper)
33
+ const amountA = clamped >= sqrtUpper
34
+ ? 0n
35
+ : (liquidity * (sqrtUpper - clamped) * Q64) / (clamped * sqrtUpper);
36
+ // amountB = L * (√p - √lower)
37
+ const amountB = clamped <= sqrtLower ? 0n : (liquidity * (clamped - sqrtLower)) / Q64;
38
+ return { amountA, amountB };
39
+ }
40
+ export const bluefinSpot = createSuiVenueAdapter({
41
+ id: "bluefin-spot",
42
+ async read(ref, account, reader) {
43
+ // Bluefin spot AMM. A position is an owned `Position` object naming its pool, so
44
+ // an account's stake in one venue is the sum of the positions it owns there.
45
+ const pools = await reader.objects([ref.venue]);
46
+ const pool = pools[ref.venue];
47
+ if (!pool)
48
+ throw new Error(`bluefin-spot: pool ${ref.venue} not found on chain`);
49
+ // Positions come from the same package as the pool, so derive the type
50
+ // rather than pin a package that moves on every contract upgrade.
51
+ const positionType = ref.shareToken ?? `${pool.type.split("::")[0]}::position::Position`;
52
+ const owned = await reader.ownedObjects(account, positionType);
53
+ const inPool = owned.filter(o => o.json.pool_id === ref.venue);
54
+ if (inPool.length === 0)
55
+ return undefined;
56
+ const [typeA, typeB] = typeArguments(pool.type);
57
+ const wantsA = coinTypesMatch(typeA, ref.coin);
58
+ if (!wantsA && !coinTypesMatch(typeB, ref.coin)) {
59
+ throw new Error(`bluefin-spot: pool ${ref.venue} does not hold ${ref.coin}`);
60
+ }
61
+ // A missing field reads as 0, which clamps every position to the lower
62
+ // tick and returns a plausible but wrong amount. Fail instead.
63
+ const sqrtPrice = toBigInt(pool.json.current_sqrt_price);
64
+ if (sqrtPrice === 0n) {
65
+ throw new Error(`bluefin-spot: pool ${ref.venue} has no current_sqrt_price`);
66
+ }
67
+ let total = 0n;
68
+ for (const position of inPool) {
69
+ const { amountA, amountB } = positionAmounts(toBigInt(position.json.liquidity), sqrtRatioAtTick(tickOf(position.json.lower_tick)), sqrtRatioAtTick(tickOf(position.json.upper_tick)), sqrtPrice);
70
+ total += wantsA ? amountA : amountB;
71
+ }
72
+ // A position wholly outside the current range holds none of this side.
73
+ // Reporting zero would render an empty row rather than no row.
74
+ if (total === 0n)
75
+ return undefined;
76
+ return total;
77
+ }
78
+ });
@@ -0,0 +1 @@
1
+ export declare const current: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,70 @@
1
+ import { coinTypesMatch, decimalToBaseUnits, tableId, toBigInt, typeArguments } from "../sui.js";
2
+ import { createSuiVenueAdapter } from "./factory.js";
3
+ export const current = createSuiVenueAdapter({
4
+ id: "current",
5
+ async read(ref, account, reader) {
6
+ // Current. An account holds an `obligation::ObligationOwnerCap` naming its
7
+ // obligation, whose cToken balances hang off a dynamic-field table keyed by the
8
+ // cToken type rather than sitting inline.
9
+ //
10
+ // Balances are cTokens, so they convert through the market's reserve — total
11
+ // supplied over cTokens issued — to the underlying coin.
12
+ const markets = await reader.objects([ref.venue]);
13
+ const market = markets[ref.venue];
14
+ if (!market)
15
+ throw new Error(`current: market ${ref.venue} not found on chain`);
16
+ const capType = ref.shareToken ??
17
+ `${market.type.split("::")[0]}::obligation::ObligationOwnerCap`;
18
+ const caps = await reader.ownedObjects(account, capType);
19
+ if (caps.length === 0)
20
+ return undefined;
21
+ // The reserve for our coin gives the exchange rate.
22
+ const reservesId = tableId(market.json.reserves);
23
+ if (!reservesId)
24
+ throw new Error(`current: no reserves table on ${ref.venue}`);
25
+ const reserves = await reader.dynamicFields(reservesId);
26
+ const reserve = reserves.find(entry => coinTypesMatch(String(entry.name ?? ""), ref.coin));
27
+ if (!reserve)
28
+ return undefined;
29
+ const reserveValue = reserve.value;
30
+ const supplied = toBigInt(reserveValue.cash) + decimalToBaseUnits(reserveValue.debt);
31
+ const ctokenSupply = toBigInt(reserveValue.total_supply);
32
+ if (ctokenSupply === 0n)
33
+ return undefined;
34
+ let held = 0n;
35
+ for (const cap of caps) {
36
+ // The cap is not parameterised by market, so an account active in
37
+ // several Current markets owns several. Both the cap and the
38
+ // obligation name the market; filtering here skips fetching the
39
+ // obligations that belong to the others.
40
+ const capMarket = cap.json.market_id;
41
+ if (capMarket && capMarket !== ref.venue)
42
+ continue;
43
+ const obligationId = cap.json.obligation_id;
44
+ if (!obligationId)
45
+ continue;
46
+ const obligations = await reader.objects([obligationId]);
47
+ const obligation = obligations[obligationId];
48
+ if (!obligation)
49
+ continue;
50
+ if (obligation.json.lending_market_id !== ref.venue)
51
+ continue;
52
+ const ctokens = obligation.json.ctokens;
53
+ const valuesId = tableId(ctokens?.ctoken_values);
54
+ if (!valuesId)
55
+ continue;
56
+ for (const entry of await reader.dynamicFields(valuesId)) {
57
+ // Keys are `CToken<MarketType, coin>` — the market type comes
58
+ // first, so the coin is the last type argument, not the first.
59
+ const args = typeArguments(String(entry.name ?? ""));
60
+ const wrapped = args[args.length - 1];
61
+ if (!wrapped || !coinTypesMatch(wrapped, ref.coin))
62
+ continue;
63
+ held += toBigInt(entry.value);
64
+ }
65
+ }
66
+ if (held === 0n)
67
+ return undefined;
68
+ return (held * supplied) / ctokenSupply;
69
+ }
70
+ });
@@ -0,0 +1 @@
1
+ export declare const openCover: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,8 @@
1
+ import { createEvmVenueAdapter } from "./factory.js";
2
+ // EVM protocols whose position IS the token the account holds, one for one, so
3
+ // the amount is that token's `balanceOf(account)`.
4
+ //
5
+ // Neither Pendle nor Royco qualifies. Pendle's PT/YT/LP are not 1:1 with the
6
+ // receipt coin, and a Royco tranche vault issues 18-decimal shares that convert
7
+ // through its own claims struct. See ./pendle.ts and ./royco.ts.
8
+ export const openCover = createEvmVenueAdapter("opencover");
@@ -0,0 +1,15 @@
1
+ import { PlacementRef, ProtocolAdapter, ProtocolId, SuiReader } from "../types.js";
2
+ /**
3
+ * What `account` holds at one venue, in base units of `ref.coin`.
4
+ *
5
+ * Return `undefined` when the account has no position there — that is the
6
+ * common case and not a failure, so it is distinct from throwing.
7
+ */
8
+ export type SuiAccountReader = (ref: PlacementRef, account: string, reader: SuiReader) => Promise<bigint | undefined>;
9
+ /** A Sui protocol adapter, defined by how one account's position is read. */
10
+ export declare function createSuiVenueAdapter(options: {
11
+ id: ProtocolId;
12
+ read: SuiAccountReader;
13
+ }): ProtocolAdapter;
14
+ /** An EVM protocol adapter. */
15
+ export declare function createEvmVenueAdapter(id: ProtocolId): ProtocolAdapter;
@@ -0,0 +1,62 @@
1
+ import { asContractAddress, encodeBalanceOf, decodeUint256 } from "../evm.js";
2
+ /** A Sui protocol adapter, defined by how one account's position is read. */
3
+ export function createSuiVenueAdapter(options) {
4
+ return {
5
+ id: options.id,
6
+ chain: "sui",
7
+ // async so a pointer source that throws synchronously still lands in
8
+ // `failures` rather than rejecting the whole call.
9
+ async discover(coin, ctx) {
10
+ return ctx.pointers.placements(options.id, coin);
11
+ },
12
+ async read(refs, account, ctx) {
13
+ if (!ctx.sui)
14
+ throw new Error("no sui reader configured");
15
+ const reader = ctx.sui;
16
+ const readAt = new Date().toISOString();
17
+ const resolved = await Promise.all(refs.map(async (ref) => {
18
+ const amount = await options.read(ref, account, reader);
19
+ if (amount === undefined)
20
+ return undefined;
21
+ return { ...ref, amount: amount.toString(), readAt };
22
+ }));
23
+ // A venue the account is not in simply does not appear.
24
+ return resolved.filter((p) => p !== undefined);
25
+ }
26
+ };
27
+ }
28
+ /** An EVM protocol adapter. */
29
+ export function createEvmVenueAdapter(id) {
30
+ // Every EVM venue in scope hands the account a token to represent its
31
+ // position — an ERC-4626 vault issues its own address, a Pendle market
32
+ // issues PT/YT/LP at separate ones — so the position is that token's
33
+ // `balanceOf(account)` and the adapters need nothing protocol-specific.
34
+ return {
35
+ id,
36
+ chain: "evm",
37
+ async discover(coin, ctx) {
38
+ return ctx.pointers.placements(id, coin);
39
+ },
40
+ async read(refs, account, ctx) {
41
+ if (!ctx.evm)
42
+ throw new Error("no evm reader configured");
43
+ const reader = ctx.evm;
44
+ const readAt = new Date().toISOString();
45
+ const resolved = await Promise.all(refs.map(async (ref) => {
46
+ const chainId = ref.chain.chainId;
47
+ if (chainId === undefined) {
48
+ throw new Error(`evm placement ${ref.venue} has no chainId`);
49
+ }
50
+ // The token the account actually holds, which is the venue
51
+ // itself only when the venue issues no separate share token.
52
+ const token = asContractAddress(ref.shareToken ?? ref.venue);
53
+ const raw = await reader.call(chainId, token, encodeBalanceOf(account), ref.rpcUrl);
54
+ const amount = decodeUint256(raw);
55
+ if (amount === 0n)
56
+ return undefined;
57
+ return { ...ref, amount: amount.toString(), readAt };
58
+ }));
59
+ return resolved.filter((p) => p !== undefined);
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./factory.js";
2
+ export { alphalend } from "./alphalend.js";
3
+ export { bluefinSpot } from "./bluefin-spot.js";
4
+ export { current } from "./current.js";
5
+ export { openCover } from "./evm-protocols.js";
6
+ export { royco } from "./royco.js";
7
+ export { navi } from "./navi.js";
8
+ export { createPendleAdapter, pendle } from "./pendle.js";
9
+ export { suilend } from "./suilend.js";
10
+ /**
11
+ * Registers the adapters that ship with the SDK. Call once at start-up, or
12
+ * register a subset by hand to keep the surface small.
13
+ */
14
+ export declare function registerBundledAdapters(): void;
@@ -0,0 +1,25 @@
1
+ import { registerAdapter } from "../registry.js";
2
+ import { alphalend } from "./alphalend.js";
3
+ import { bluefinSpot } from "./bluefin-spot.js";
4
+ import { current } from "./current.js";
5
+ import { openCover } from "./evm-protocols.js";
6
+ import { royco } from "./royco.js";
7
+ import { navi } from "./navi.js";
8
+ import { pendle } from "./pendle.js";
9
+ import { suilend } from "./suilend.js";
10
+ export * from "./factory.js";
11
+ export { alphalend } from "./alphalend.js";
12
+ export { bluefinSpot } from "./bluefin-spot.js";
13
+ export { current } from "./current.js";
14
+ export { openCover } from "./evm-protocols.js";
15
+ export { royco } from "./royco.js";
16
+ export { navi } from "./navi.js";
17
+ export { createPendleAdapter, pendle } from "./pendle.js";
18
+ export { suilend } from "./suilend.js";
19
+ /**
20
+ * Registers the adapters that ship with the SDK. Call once at start-up, or
21
+ * register a subset by hand to keep the surface small.
22
+ */
23
+ export function registerBundledAdapters() {
24
+ [bluefinSpot, suilend, alphalend, current, navi, pendle, royco, openCover].forEach(registerAdapter);
25
+ }
@@ -0,0 +1 @@
1
+ export declare const navi: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,53 @@
1
+ import { bcsAddress, coinTypesMatch, tableId, toBigInt } from "../sui.js";
2
+ import { createSuiVenueAdapter } from "./factory.js";
3
+ /** Navi's fixed-point scale for its supply/borrow indexes. */
4
+ const RAY = 10n ** 27n;
5
+ /** Navi normalises every reserve to 9 decimals, whatever the coin's own are. */
6
+ const NAVI_DECIMALS = 9;
7
+ export const navi = createSuiVenueAdapter({
8
+ id: "navi",
9
+ async read(ref, account, reader) {
10
+ // Navi. A lender's balance lives in the shared `storage::Storage` object, in a
11
+ // per-reserve table keyed by depositor address, rather than in anything the
12
+ // account owns — so there is no cap or position object to walk from.
13
+ //
14
+ // That table is keyed by address and grows with the depositor count, so it is
15
+ // addressed by key rather than paged.
16
+ //
17
+ // Balances are stored scaled by the reserve's supply index and normalised to 9
18
+ // decimals, so they convert back through both.
19
+ const storages = await reader.objects([ref.venue]);
20
+ const storage = storages[ref.venue];
21
+ if (!storage)
22
+ throw new Error(`navi: storage ${ref.venue} not found on chain`);
23
+ const reservesId = tableId(storage.json.reserves);
24
+ if (!reservesId)
25
+ throw new Error(`navi: no reserves table on ${ref.venue}`);
26
+ // Navi runs several Storage objects and a coin may be listed in more
27
+ // than one, so only the reserve in this venue counts.
28
+ const reserves = await reader.dynamicFields(reservesId);
29
+ const reserve = reserves.find(entry => coinTypesMatch(String(entry.value?.coin_type ?? ""), ref.coin));
30
+ if (!reserve)
31
+ return undefined;
32
+ const value = reserve.value;
33
+ const supply = value.supply_balance;
34
+ const userStateId = tableId(supply?.user_state);
35
+ if (!userStateId)
36
+ return undefined;
37
+ const scaled = toBigInt(await reader.dynamicField(userStateId, {
38
+ type: "address",
39
+ bcs: bcsAddress(account)
40
+ }));
41
+ if (scaled === 0n)
42
+ return undefined;
43
+ const index = toBigInt(value.current_supply_index) || RAY;
44
+ // Balances are held at NAVI_DECIMALS regardless of the coin, so rescale
45
+ // in whichever direction the coin needs rather than clamping at zero —
46
+ // clamping silently under-reports a coin with more decimals than navi.
47
+ const shift = NAVI_DECIMALS - ref.decimals;
48
+ const amount = (scaled * index) / RAY;
49
+ return shift >= 0
50
+ ? amount / 10n ** BigInt(shift)
51
+ : amount * 10n ** BigInt(-shift);
52
+ }
53
+ });
@@ -0,0 +1,9 @@
1
+ import { ProtocolAdapter } from "../types.js";
2
+ export interface PendleOptions {
3
+ /** Overrides the public API, for a proxy or a test double. */
4
+ baseUrl?: string;
5
+ /** Injection seam for tests; defaults to `fetch`. */
6
+ load?: (url: string) => Promise<unknown>;
7
+ }
8
+ export declare function createPendleAdapter(options?: PendleOptions): ProtocolAdapter;
9
+ export declare const pendle: ProtocolAdapter;
@@ -0,0 +1,108 @@
1
+ const PENDLE_API = "https://api-v2.pendle.finance/core";
2
+ /**
3
+ * Converts a decimal value to base units without going through a float at the
4
+ * end, so a large `decimals` does not turn the integer part into garbage.
5
+ */
6
+ function toBaseUnits(value, decimals) {
7
+ if (!Number.isFinite(value) || value <= 0)
8
+ return 0n;
9
+ const [whole, fraction = ""] = value.toFixed(decimals).split(".");
10
+ return BigInt(whole + fraction.padEnd(decimals, "0").slice(0, decimals));
11
+ }
12
+ export function createPendleAdapter(options = {}) {
13
+ // Pendle.
14
+ //
15
+ // A Pendle position is not held in the receipt coin: splitting eEARN yields PT
16
+ // and YT, and providing liquidity yields an LP token. None of those is 1:1 with
17
+ // eEARN before maturity, so a raw `balanceOf` would be a wrong number rather
18
+ // than a useful one.
19
+ //
20
+ // So the amount reported here is the position's **underlying-equivalent**: what
21
+ // Pendle values the PT + YT + LP legs at, divided by the coin's own price. Two
22
+ // consequences worth knowing, both deliberate:
23
+ // - it is market-priced, so it moves with the AMM rather than being a fixed
24
+ // claim, and two reads seconds apart can differ;
25
+ // - it comes from Pendle's API, so unlike every other adapter this `read`
26
+ // depends on something other than chain state.
27
+ //
28
+ // Bare SY balances are not counted. SY is not tied to one market and is rare to
29
+ // hold directly; omitting it can only under-report, never misreport.
30
+ const baseUrl = (options.baseUrl ?? PENDLE_API).replace(/\/$/, "");
31
+ const load = options.load ??
32
+ (async (url) => {
33
+ const response = await fetch(url, {
34
+ headers: { Accept: "application/json" }
35
+ });
36
+ if (!response.ok)
37
+ throw new Error(`pendle api ${response.status}`);
38
+ return response.json();
39
+ });
40
+ return {
41
+ id: "pendle",
42
+ chain: "evm",
43
+ async discover(coin, ctx) {
44
+ return ctx.pointers.placements("pendle", coin);
45
+ },
46
+ // ctx is unused: Pendle values a position through its own API, not
47
+ // through the sui/evm readers.
48
+ async read(refs, account) {
49
+ if (refs.length === 0)
50
+ return [];
51
+ const readAt = new Date().toISOString();
52
+ const positions = (await load(`${baseUrl}/v1/dashboard/positions/database/${account}`));
53
+ // Resolve every chain id up front, so nothing downstream has to
54
+ // assert one is present.
55
+ const targets = refs.map(ref => {
56
+ const chainId = ref.chain.chainId;
57
+ if (chainId === undefined) {
58
+ throw new Error(`pendle placement ${ref.venue} has no chainId`);
59
+ }
60
+ return { ref, chainId };
61
+ });
62
+ // One price call per chain, covering every coin asked about.
63
+ const coinsByChain = new Map();
64
+ for (const { ref, chainId } of targets) {
65
+ const coins = coinsByChain.get(chainId) ?? new Set();
66
+ coins.add(ref.coin.toLowerCase());
67
+ coinsByChain.set(chainId, coins);
68
+ }
69
+ const prices = new Map();
70
+ await Promise.all([...coinsByChain].map(async ([chainId, coins]) => {
71
+ const body = (await load(`${baseUrl}/v1/${chainId}/assets/prices?addresses=${[...coins].join(",")}`));
72
+ for (const [address, price] of Object.entries(body.prices ?? {})) {
73
+ prices.set(`${chainId}-${address.toLowerCase()}`, price);
74
+ }
75
+ }));
76
+ const resolved = targets.map(({ ref, chainId }) => {
77
+ const chain = (positions.positions ?? []).find(p => p.chainId === chainId);
78
+ if (!chain)
79
+ return undefined;
80
+ // Closed positions are scanned too: Pendle files a position
81
+ // there once exited, but a residual balance still counts.
82
+ const marketId = `${chainId}-${ref.venue.toLowerCase()}`;
83
+ const legs = [
84
+ ...(chain.openPositions ?? []),
85
+ ...(chain.closedPositions ?? [])
86
+ ].filter(p => p.marketId?.toLowerCase() === marketId);
87
+ if (legs.length === 0)
88
+ return undefined;
89
+ const usd = legs.reduce((sum, p) => sum +
90
+ (p.pt?.valuation ?? 0) +
91
+ (p.yt?.valuation ?? 0) +
92
+ (p.lp?.valuation ?? 0), 0);
93
+ if (usd <= 0)
94
+ return undefined;
95
+ const price = prices.get(`${chainId}-${ref.coin.toLowerCase()}`);
96
+ if (!price) {
97
+ throw new Error(`pendle: no price for ${ref.coin} on chain ${chainId}`);
98
+ }
99
+ const amount = toBaseUnits(usd / price, ref.decimals);
100
+ if (amount === 0n)
101
+ return undefined;
102
+ return { ...ref, amount: amount.toString(), readAt };
103
+ });
104
+ return resolved.filter((p) => p !== undefined);
105
+ }
106
+ };
107
+ }
108
+ export const pendle = createPendleAdapter();
@@ -0,0 +1,2 @@
1
+ import { ProtocolAdapter } from "../types.js";
2
+ export declare const royco: ProtocolAdapter;
@@ -0,0 +1,49 @@
1
+ import { asContractAddress, decodeUint256, encodeBalanceOf } from "../evm.js";
2
+ /** `convertToAssets(uint256)` */
3
+ const CONVERT_TO_ASSETS = "0x07a2d13a";
4
+ const WORD = 64;
5
+ /**
6
+ * What `account` holds at one Royco tranche vault, in base units of `ref.coin`.
7
+ */
8
+ async function trancheAssets(ref, account, ctx) {
9
+ if (!ctx.evm)
10
+ throw new Error("no evm reader configured");
11
+ const chainId = ref.chain.chainId;
12
+ if (chainId === undefined) {
13
+ throw new Error(`royco placement ${ref.venue} has no chainId`);
14
+ }
15
+ const vault = asContractAddress(ref.shareToken ?? ref.venue);
16
+ const shares = decodeUint256(await ctx.evm.call(chainId, vault, encodeBalanceOf(account), ref.rpcUrl));
17
+ if (shares === 0n)
18
+ return undefined;
19
+ // A tranche vault is not ERC-4626 despite the selector: convertToAssets
20
+ // returns AssetClaims { stAssets, jtAssets, nav }. The first two are the
21
+ // position in the deposit token's own decimals and are what the holder can
22
+ // claim; nav is a USD figure on a different scale. Taking one word — the
23
+ // obvious reading of a 4626 — reports a fraction of the position.
24
+ const raw = await ctx.evm.call(chainId, vault, `${CONVERT_TO_ASSETS}${shares.toString(16).padStart(WORD, "0")}`, ref.rpcUrl);
25
+ const body = raw.startsWith("0x") ? raw.slice(2) : raw;
26
+ if (body.length < WORD * 3) {
27
+ throw new Error(`royco: convertToAssets returned ${body.length / 2} bytes for ${ref.venue}`);
28
+ }
29
+ const stAssets = BigInt(`0x${body.slice(0, WORD)}`);
30
+ const jtAssets = BigInt(`0x${body.slice(WORD, WORD * 2)}`);
31
+ return stAssets + jtAssets;
32
+ }
33
+ export const royco = {
34
+ id: "royco",
35
+ chain: "evm",
36
+ async discover(coin, ctx) {
37
+ return ctx.pointers.placements("royco", coin);
38
+ },
39
+ async read(refs, account, ctx) {
40
+ const readAt = new Date().toISOString();
41
+ const resolved = await Promise.all(refs.map(async (ref) => {
42
+ const amount = await trancheAssets(ref, account, ctx);
43
+ if (amount === undefined)
44
+ return undefined;
45
+ return { ...ref, amount: amount.toString(), readAt };
46
+ }));
47
+ return resolved.filter((p) => p !== undefined);
48
+ }
49
+ };
@@ -0,0 +1 @@
1
+ export declare const suilend: import("../types.js").ProtocolAdapter;
@@ -0,0 +1,57 @@
1
+ import { coinTypesMatch, decimalToBaseUnits, toBigInt } from "../sui.js";
2
+ import { createSuiVenueAdapter } from "./factory.js";
3
+ export const suilend = createSuiVenueAdapter({
4
+ id: "suilend",
5
+ async read(ref, account, reader) {
6
+ // Suilend. An account holds an `ObligationOwnerCap` pointing at its obligation,
7
+ // which lists one deposit per coin.
8
+ //
9
+ // Deposits are denominated in cTokens, so they are converted through the
10
+ // reserve's exchange rate — total supplied over cTokens issued — to the
11
+ // underlying coin. Reading the raw cToken figure would under-report by whatever
12
+ // interest has accrued.
13
+ const markets = await reader.objects([ref.venue]);
14
+ const market = markets[ref.venue];
15
+ if (!market) {
16
+ throw new Error(`suilend: lending market ${ref.venue} not found on chain`);
17
+ }
18
+ const capType = ref.shareToken ??
19
+ `${market.type.split("::")[0]}::lending_market::ObligationOwnerCap`;
20
+ const caps = await reader.ownedObjects(account, capType);
21
+ if (caps.length === 0)
22
+ return undefined;
23
+ const reserves = (market.json.reserves ?? []);
24
+ const reserve = reserves.find(r => coinTypesMatch(r.coin_type ?? "", ref.coin));
25
+ if (!reserve)
26
+ return undefined;
27
+ const supplied = toBigInt(reserve.available_amount) +
28
+ decimalToBaseUnits(reserve.borrowed_amount);
29
+ const ctokens = toBigInt(reserve.ctoken_supply);
30
+ if (ctokens === 0n)
31
+ return undefined;
32
+ const obligationIds = caps
33
+ .map(cap => cap.json.obligation_id)
34
+ .filter((id) => Boolean(id));
35
+ const obligations = await reader.objects(obligationIds);
36
+ let deposited = 0n;
37
+ for (const id of obligationIds) {
38
+ const obligation = obligations[id];
39
+ if (!obligation)
40
+ continue;
41
+ // An account can hold obligations across several Suilend markets,
42
+ // and the same coin can be listed in more than one. Without this the
43
+ // deposits of every market would be summed into each venue's row.
44
+ if (obligation.json.lending_market_id !== ref.venue)
45
+ continue;
46
+ const deposits = (obligation.json.deposits ?? []);
47
+ for (const deposit of deposits) {
48
+ if (!coinTypesMatch(deposit.coin_type ?? "", ref.coin))
49
+ continue;
50
+ deposited += toBigInt(deposit.deposited_ctoken_amount);
51
+ }
52
+ }
53
+ if (deposited === 0n)
54
+ return undefined;
55
+ return (deposited * supplied) / ctokens;
56
+ }
57
+ });