@ember-finance/sdk 2.12.0-ccip.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.
- package/dist/src/abis/index.d.ts +1 -3
- package/dist/src/abis/index.js +1 -4
- package/dist/src/api/v2/models/evm-contracts.d.ts +0 -9
- package/dist/src/api/v2/models/evm-oftadapter.d.ts +0 -11
- package/dist/src/api/v2/models/evm-oftadapter.js +1 -4
- package/dist/src/api/v2/models/evm-oftpeer.d.ts +0 -12
- package/dist/src/api/v2/models/index.d.ts +0 -3
- package/dist/src/api/v2/models/index.js +0 -3
- package/dist/src/api/v2/models/vault-parking-lot-info.d.ts +6 -0
- package/dist/src/evm-vaults/interfaces/index.d.ts +0 -39
- package/dist/src/evm-vaults/on-chain-calls/tx-builder.d.ts +1 -15
- package/dist/src/evm-vaults/on-chain-calls/tx-builder.js +0 -36
- package/dist/src/evm-vaults/on-chain-calls/user.d.ts +1 -33
- package/dist/src/evm-vaults/on-chain-calls/user.js +0 -61
- package/dist/src/evm-vaults/on-chain-calls/vault-reader.d.ts +1 -14
- package/dist/src/evm-vaults/on-chain-calls/vault-reader.js +0 -30
- package/dist/src/evm-vaults/utils/deployment-parser.d.ts +1 -27
- package/dist/src/evm-vaults/utils/deployment-parser.js +0 -37
- package/dist/src/evm-vaults/utils/index.d.ts +0 -1
- package/dist/src/evm-vaults/utils/index.js +0 -1
- package/dist/src/placements/adapters/alphalend.d.ts +1 -0
- package/dist/src/placements/adapters/alphalend.js +58 -0
- package/dist/src/placements/adapters/bluefin-spot.d.ts +1 -0
- package/dist/src/placements/adapters/bluefin-spot.js +78 -0
- package/dist/src/placements/adapters/current.d.ts +1 -0
- package/dist/src/placements/adapters/current.js +70 -0
- package/dist/src/placements/adapters/evm-protocols.d.ts +1 -0
- package/dist/src/placements/adapters/evm-protocols.js +8 -0
- package/dist/src/placements/adapters/factory.d.ts +15 -0
- package/dist/src/placements/adapters/factory.js +62 -0
- package/dist/src/placements/adapters/index.d.ts +14 -0
- package/dist/src/placements/adapters/index.js +25 -0
- package/dist/src/placements/adapters/navi.d.ts +1 -0
- package/dist/src/placements/adapters/navi.js +53 -0
- package/dist/src/placements/adapters/pendle.d.ts +9 -0
- package/dist/src/placements/adapters/pendle.js +108 -0
- package/dist/src/placements/adapters/royco.d.ts +2 -0
- package/dist/src/placements/adapters/royco.js +49 -0
- package/dist/src/placements/adapters/suilend.d.ts +1 -0
- package/dist/src/placements/adapters/suilend.js +57 -0
- package/dist/src/placements/batch.d.ts +70 -0
- package/dist/src/placements/batch.js +197 -0
- package/dist/src/placements/client.d.ts +17 -0
- package/dist/src/placements/client.js +194 -0
- package/dist/src/placements/evm.d.ts +29 -0
- package/dist/src/placements/evm.js +280 -0
- package/dist/src/placements/index.d.ts +9 -0
- package/dist/src/placements/index.js +9 -0
- package/dist/src/placements/pointers.d.ts +24 -0
- package/dist/src/placements/pointers.js +126 -0
- package/dist/src/placements/registry.d.ts +11 -0
- package/dist/src/placements/registry.js +19 -0
- package/dist/src/placements/sui.d.ts +47 -0
- package/dist/src/placements/sui.js +302 -0
- package/dist/src/placements/types.d.ts +147 -0
- package/dist/src/placements/types.js +1 -0
- package/dist/src/placements/vaults.d.ts +15 -0
- package/dist/src/placements/vaults.js +97 -0
- package/package.json +6 -1
- package/dist/src/abis/CcipRouter.json +0 -58
- package/dist/src/api/v2/models/evm-ccip-lane.d.ts +0 -73
- package/dist/src/api/v2/models/evm-ccip-lane.js +0 -14
- package/dist/src/api/v2/models/evm-ccip-rate-limits.d.ts +0 -36
- package/dist/src/api/v2/models/evm-ccip-rate-limits.js +0 -14
- package/dist/src/api/v2/models/evm-ccip-token-pool.d.ts +0 -122
- package/dist/src/api/v2/models/evm-ccip-token-pool.js +0 -17
- package/dist/src/evm-vaults/utils/ccip.d.ts +0 -23
- package/dist/src/evm-vaults/utils/ccip.js +0 -32
|
@@ -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,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
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface TransportRequest {
|
|
2
|
+
items: number;
|
|
3
|
+
attempt: number;
|
|
4
|
+
status: number | "network-error";
|
|
5
|
+
}
|
|
6
|
+
export type BatchResult<T> = {
|
|
7
|
+
ok: true;
|
|
8
|
+
value: T;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: Error;
|
|
12
|
+
};
|
|
13
|
+
export declare class TransientError extends Error {
|
|
14
|
+
readonly retryAfterMs?: number;
|
|
15
|
+
constructor(message: string, retryAfterMs?: number);
|
|
16
|
+
}
|
|
17
|
+
export declare class ChunkTooLargeError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class AbortedError extends Error {
|
|
21
|
+
constructor(message?: string);
|
|
22
|
+
}
|
|
23
|
+
export declare function retryAfterMs(header: string | null | undefined): number | undefined;
|
|
24
|
+
export declare function isTransientStatus(status: number): boolean;
|
|
25
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
26
|
+
export interface BatcherOptions<Item, Result> {
|
|
27
|
+
send: (items: Item[], attempt: number) => Promise<BatchResult<Result>[]>;
|
|
28
|
+
key: (item: Item) => string;
|
|
29
|
+
weight?: (item: Item) => number;
|
|
30
|
+
bytes?: (item: Item) => number;
|
|
31
|
+
maxWeight?: number;
|
|
32
|
+
maxBytes?: number;
|
|
33
|
+
concurrency?: number;
|
|
34
|
+
retries?: number;
|
|
35
|
+
baseDelayMs?: number;
|
|
36
|
+
maxDelayMs?: number;
|
|
37
|
+
sleep?: (ms: number) => Promise<void>;
|
|
38
|
+
schedule?: (flush: () => void) => void;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
export declare class Batcher<Item, Result> {
|
|
42
|
+
private readonly send;
|
|
43
|
+
private readonly key;
|
|
44
|
+
private readonly weight;
|
|
45
|
+
private readonly bytes;
|
|
46
|
+
private readonly maxWeight;
|
|
47
|
+
private readonly maxBytes;
|
|
48
|
+
private readonly concurrency;
|
|
49
|
+
private readonly retries;
|
|
50
|
+
private readonly baseDelayMs;
|
|
51
|
+
private readonly maxDelayMs;
|
|
52
|
+
private readonly sleep;
|
|
53
|
+
private readonly schedule;
|
|
54
|
+
private readonly signal?;
|
|
55
|
+
private readonly queue;
|
|
56
|
+
private readonly inFlight;
|
|
57
|
+
private readonly waiting;
|
|
58
|
+
private active;
|
|
59
|
+
private scheduled;
|
|
60
|
+
constructor(options: BatcherOptions<Item, Result>);
|
|
61
|
+
enqueue(item: Item): Promise<Result>;
|
|
62
|
+
private scheduleFlush;
|
|
63
|
+
private flush;
|
|
64
|
+
private chunk;
|
|
65
|
+
private run;
|
|
66
|
+
private retryLater;
|
|
67
|
+
private backoff;
|
|
68
|
+
private acquire;
|
|
69
|
+
private release;
|
|
70
|
+
}
|