Skip to content

Commit f7cc477

Browse files
g1nt0kiWhanod
andauthored
Add astrolend adapter and refactor code (DefiLlama#12975)
Co-authored-by: Whanod <[email protected]>
1 parent f055266 commit f7cc477

File tree

5 files changed

+291
-17
lines changed

5 files changed

+291
-17
lines changed

projects/astrolend/idl.json

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
{
2+
"events": [],
3+
"accounts": [{"discriminator": [142, 49, 166, 242, 50, 66, 97, 188], "name": "Bank"}],
4+
"address": "Astro1oWvtB7cBTwi3efLMFB47WXx7DJDQeoxi235kA",
5+
"instructions": [],
6+
"metadata": {"description": "Created with Anchor", "name": "astrolend", "spec": "0.1.0", "version": "0.1.0"},
7+
"types": [
8+
{
9+
"name": "Bank",
10+
"repr": {"kind": "c"},
11+
"serialization": "bytemuckunsafe",
12+
"type": {
13+
"fields": [
14+
{"name": "mint", "type": "pubkey"},
15+
{"name": "mint_decimals", "type": "u8"},
16+
{"name": "group", "type": "pubkey"},
17+
{"name": "_pad0", "type": {"array": ["u8", 7]}},
18+
{"name": "asset_share_value", "type": {"defined": {"name": "WrappedI80F48"}}},
19+
{"name": "liability_share_value", "type": {"defined": {"name": "WrappedI80F48"}}},
20+
{"name": "liquidity_vault", "type": "pubkey"},
21+
{"name": "liquidity_vault_bump", "type": "u8"},
22+
{"name": "liquidity_vault_authority_bump", "type": "u8"},
23+
{"name": "insurance_vault", "type": "pubkey"},
24+
{"name": "insurance_vault_bump", "type": "u8"},
25+
{"name": "insurance_vault_authority_bump", "type": "u8"},
26+
{"name": "_pad1", "type": {"array": ["u8", 4]}},
27+
{"name": "collected_insurance_fees_outstanding", "type": {"defined": {"name": "WrappedI80F48"}}},
28+
{"name": "fee_vault", "type": "pubkey"},
29+
{"name": "fee_vault_bump", "type": "u8"},
30+
{"name": "fee_vault_authority_bump", "type": "u8"},
31+
{"name": "_pad2", "type": {"array": ["u8", 6]}},
32+
{"name": "collected_group_fees_outstanding", "type": {"defined": {"name": "WrappedI80F48"}}},
33+
{"name": "total_liability_shares", "type": {"defined": {"name": "WrappedI80F48"}}},
34+
{"name": "total_asset_shares", "type": {"defined": {"name": "WrappedI80F48"}}},
35+
{"name": "last_update", "type": "i64"},
36+
{"name": "config", "type": {"defined": {"name": "BankConfig"}}},
37+
{
38+
"docs": [
39+
"Bank Config Flags",
40+
"",
41+
"- EMISSIONS_FLAG_BORROW_ACTIVE: 1",
42+
"- EMISSIONS_FLAG_LENDING_ACTIVE: 2",
43+
"- PERMISSIONLESS_BAD_DEBT_SETTLEMENT: 4",
44+
""
45+
],
46+
"name": "flags",
47+
"type": "u64"
48+
},
49+
{
50+
"docs": [
51+
"Emissions APR.",
52+
"Number of emitted tokens (emissions_mint) per 1e(bank.mint_decimal) tokens (bank mint) (native amount) per 1 YEAR."
53+
],
54+
"name": "emissions_rate",
55+
"type": "u64"
56+
},
57+
{"name": "emissions_remaining", "type": {"defined": {"name": "WrappedI80F48"}}},
58+
{"name": "emissions_mint", "type": "pubkey"},
59+
{"name": "_padding_0", "type": {"array": [{"array": ["u64", 2]}, 28]}},
60+
{"name": "_padding_1", "type": {"array": [{"array": ["u64", 2]}, 32]}}
61+
],
62+
"kind": "struct"
63+
}
64+
},
65+
{
66+
"name": "WrappedI80F48",
67+
"repr": {"align": 8, "kind": "c"},
68+
"serialization": "bytemuck",
69+
"type": {"fields": [{"name": "value", "type": {"array": ["u8", 16]}}], "kind": "struct"}
70+
},
71+
{
72+
"docs": ["TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?)"],
73+
"name": "BankConfig",
74+
"repr": {"kind": "c"},
75+
"serialization": "bytemuckunsafe",
76+
"type": {
77+
"fields": [
78+
{"name": "asset_weight_init", "type": {"defined": {"name": "WrappedI80F48"}}},
79+
{"name": "asset_weight_maint", "type": {"defined": {"name": "WrappedI80F48"}}},
80+
{"name": "liability_weight_init", "type": {"defined": {"name": "WrappedI80F48"}}},
81+
{"name": "liability_weight_maint", "type": {"defined": {"name": "WrappedI80F48"}}},
82+
{"name": "deposit_limit", "type": "u64"},
83+
{"name": "interest_rate_config", "type": {"defined": {"name": "InterestRateConfig"}}},
84+
{"name": "operational_state", "type": {"defined": {"name": "BankOperationalState"}}},
85+
{"name": "oracle_setup", "type": {"defined": {"name": "OracleSetup"}}},
86+
{"name": "oracle_keys", "type": {"array": ["pubkey", 5]}},
87+
{"name": "_pad0", "type": {"array": ["u8", 6]}},
88+
{"name": "borrow_limit", "type": "u64"},
89+
{"name": "risk_tier", "type": {"defined": {"name": "RiskTier"}}},
90+
{"name": "auto_padding_0", "type": {"array": ["u8", 7]}},
91+
{"name": "_pad1", "type": {"array": ["u8", 7]}},
92+
{
93+
"docs": [
94+
"USD denominated limit for calculating asset value for initialization astrol requirements.",
95+
"Example, if total SOL deposits are equal to $1M and the limit it set to $500K,",
96+
"then SOL assets will be discounted by 50%.",
97+
"",
98+
"In other words the max value of liabilities that can be backed by the asset is $500K.",
99+
"This is useful for limiting the damage of orcale attacks.",
100+
"",
101+
"Value is UI USD value, for example value 100 -> $100"
102+
],
103+
"name": "total_asset_value_init_limit",
104+
"type": "u64"
105+
},
106+
{"docs": ["Time window in seconds for the oracle price feed to be considered live."], "name": "oracle_max_age", "type": "u16"},
107+
{"name": "_padding", "type": {"array": ["u8", 38]}}
108+
],
109+
"kind": "struct"
110+
}
111+
},
112+
{
113+
"name": "InterestRateConfig",
114+
"repr": {"kind": "c"},
115+
"serialization": "bytemuck",
116+
"type": {
117+
"fields": [
118+
{"name": "optimal_utilization_rate", "type": {"defined": {"name": "WrappedI80F48"}}},
119+
{"name": "plateau_interest_rate", "type": {"defined": {"name": "WrappedI80F48"}}},
120+
{"name": "max_interest_rate", "type": {"defined": {"name": "WrappedI80F48"}}},
121+
{"name": "insurance_fee_fixed_apr", "type": {"defined": {"name": "WrappedI80F48"}}},
122+
{"name": "insurance_ir_fee", "type": {"defined": {"name": "WrappedI80F48"}}},
123+
{"name": "protocol_fixed_fee_apr", "type": {"defined": {"name": "WrappedI80F48"}}},
124+
{"name": "protocol_ir_fee", "type": {"defined": {"name": "WrappedI80F48"}}},
125+
{"name": "_padding", "type": {"array": [{"array": ["u64", 2]}, 8]}}
126+
],
127+
"kind": "struct"
128+
}
129+
},
130+
{
131+
"name": "BankOperationalState",
132+
"repr": {"kind": "rust"},
133+
"type": {"kind": "enum", "variants": [{"name": "Paused"}, {"name": "Operational"}, {"name": "ReduceOnly"}]}
134+
},
135+
{
136+
"name": "OracleSetup",
137+
"repr": {"kind": "rust"},
138+
"type": {
139+
"kind": "enum",
140+
"variants": [{"name": "None"}, {"name": "PythLegacy"}, {"name": "SwitchboardV2"}, {"name": "PythPushOracle"}, {"name": "SwitchboardPull"}]
141+
}
142+
},
143+
{"name": "RiskTier", "repr": {"kind": "rust"}, "type": {"kind": "enum", "variants": [{"name": "Collateral"}, {"name": "Isolated"}]}}
144+
],
145+
"errors": []
146+
}

projects/astrolend/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const { Program } = require("@coral-xyz/anchor");
2+
const { getProvider, sumTokens2 } = require("../helper/solana");
3+
const idl = require('./idl.json')
4+
const wrappedI80F48toBigNumber = require("./utils/conversion")
5+
6+
let _banks
7+
8+
async function getBanks() {
9+
if (_banks) return _banks
10+
const provider = getProvider('eclipse')
11+
const program = new Program(idl, provider)
12+
_banks = program.account.bank.all()
13+
return _banks
14+
}
15+
16+
async function tvl(api) {
17+
const banks = await getBanks()
18+
return sumTokens2({ api, tokenAccounts: banks.map(bank => bank.account.liquidityVault) });
19+
}
20+
21+
async function borrowed(api) {
22+
const banks = await getBanks()
23+
24+
banks.forEach(bank => {
25+
api.add(bank.account.mint.toString(), wrappedI80F48toBigNumber(bank.account.totalLiabilityShares.value))
26+
})
27+
}
28+
29+
module.exports = {
30+
timetravel: false,
31+
eclipse: { tvl, borrowed, },
32+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
function decodeI80F48FromHex(hex, fractionalBytes = 6, signChar = "") {
2+
// 1) Parse the hex as BigInt
3+
let value = BigInt(hex);
4+
5+
// 2) Apply sign if needed
6+
if (signChar === "-") {
7+
value = -value;
8+
}
9+
10+
// 3) Separate integer/fractional bits
11+
// Typically I80F48 -> 48 fractional bits = 6 bytes * 8 bits/byte.
12+
const fractionBits = 8n * BigInt(fractionalBytes);
13+
14+
// integerPart = value >> 48
15+
let integerPart = value >> fractionBits;
16+
17+
// fractionalPart = value & ((1 << 48) - 1)
18+
let fractionalPart = value & ((1n << fractionBits) - 1n);
19+
20+
// Remember if final result should be negative
21+
const isNegative = integerPart < 0n;
22+
if (isNegative) {
23+
integerPart = -integerPart;
24+
fractionalPart = -fractionalPart;
25+
// With two’s complement, you might have to do more nuance,
26+
// but typically I80F48 is stored as a signed 128-bit or so.
27+
// For many Solana-based I80F48 implementations, the fractional portion
28+
// is treated as positive magnitude once you know the sign.
29+
}
30+
31+
// 4) Convert integer part to decimal string
32+
const integerString = integerPart.toString(10);
33+
34+
// 5) Convert fractional part to decimal string by repeated "multiply by 10, divide by 2^48"
35+
let fractionString = "";
36+
const denominator = 1n << fractionBits;
37+
38+
// Ensure fractionalPart is positive for the loop
39+
if (fractionalPart < 0n) {
40+
fractionalPart = -fractionalPart;
41+
}
42+
43+
while (fractionalPart !== 0n) {
44+
fractionalPart *= 10n;
45+
const digit = fractionalPart / denominator;
46+
fractionString += digit.toString(10);
47+
fractionalPart = fractionalPart % denominator;
48+
}
49+
50+
// Combine integer + fractional
51+
let result = fractionString.length > 0
52+
? integerString + "." + fractionString
53+
: integerString;
54+
55+
// Re-apply negative if needed
56+
if (isNegative) {
57+
result = "-" + result;
58+
}
59+
60+
return result;
61+
}
62+
63+
// --------------------------------------------------------------------
64+
// USAGE EXAMPLE:
65+
66+
// Suppose we have 16 bytes in I80F48 format, with 8 fractional bytes (48 bits).
67+
// (Adjust fractionalBytes as needed for your situation.)
68+
const I80F48_FRACTIONAL_BYTES = 6; // typical for "I80F48" = 48 fractional bits
69+
70+
function wrappedI80F48toBigNumber(wrapped) {
71+
const I80F48_TOTAL_BYTES = 16
72+
let bytesLE = wrapped;
73+
if (bytesLE.length !== I80F48_TOTAL_BYTES) {
74+
throw new Error(`Expected a ${I80F48_TOTAL_BYTES}-byte buffer`);
75+
}
76+
77+
let bytesBE = bytesLE.slice();
78+
bytesBE.reverse();
79+
80+
let signChar = "";
81+
const msb = bytesBE[0];
82+
if (msb & 0x80) {
83+
signChar = "-";
84+
bytesBE = bytesBE.map((v) => ~v & 0xff);
85+
}
86+
87+
let hex = signChar + "0x" + bytesBE.map((v) => v.toString(16).padStart(2, "0")).join("");
88+
const decimalString = decodeI80F48FromHex(hex, 6)
89+
return decimalString.split(".")[0]
90+
91+
}
92+
93+
module.exports = wrappedI80F48toBigNumber

projects/helper/solana.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,21 @@ async function runInChunks(inputs, fn, { chunkSize = 99, sleepTime } = {}) {
409409
return results.flat()
410410
}
411411

412+
function i80f48ToNumber(i80f48) {
413+
if (i80f48.value) i80f48 = i80f48.value
414+
// Create a mask with the lower 48 bits set to 1
415+
const mask = BigInt((1n << 48n) - 1n)
416+
417+
// Shift right by 48 bits to get the integer part
418+
const integerPart = BigInt(i80f48) >> BigInt(48)
419+
420+
// Use bitwise AND to get the fractional part
421+
const fractionalPart = BigInt(i80f48) & mask
422+
423+
// Convert to regular numbers and add together
424+
return Number(integerPart) + Number(fractionalPart) / Number(1n << 48n)
425+
}
426+
412427
module.exports = {
413428
endpoint: endpoint(),
414429
getMultipleAccounts,
@@ -428,4 +443,5 @@ module.exports = {
428443
ASSOCIATED_TOKEN_PROGRAM_ID,
429444
TOKEN_2022_PROGRAM_ID,
430445
getAssociatedTokenAddress,
446+
i80f48ToNumber,
431447
};

projects/nxfi/index.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,21 @@
11
const { Program } = require("@project-serum/anchor");
2-
const { getProvider, sumTokens2, } = require("../helper/solana");
2+
const { getProvider, sumTokens2, i80f48ToNumber, } = require("../helper/solana");
33

44
const idl = require('./idl')
55

6-
function i80f48ToNumber(i80f48) {
7-
if (i80f48.value) i80f48 = i80f48.value
8-
// Create a mask with the lower 48 bits set to 1
9-
const mask = BigInt((1n << 48n) - 1n)
10-
11-
// Shift right by 48 bits to get the integer part
12-
const integerPart = BigInt(i80f48) >> BigInt(48)
13-
14-
// Use bitwise AND to get the fractional part
15-
const fractionalPart = BigInt(i80f48) & mask
16-
17-
// Convert to regular numbers and add together
18-
return Number(integerPart) + Number(fractionalPart) / Number(1n << 48n)
19-
}
206

217
async function tvl() {
228
const provider = getProvider()
239
const program = new Program(idl, 'NxFiv1eeKtKT6dQEP2erBwz2DSKCTdb8WSsxVDwVGJ1', provider)
2410
const reserves = await program.account.reserve.all()
2511
return sumTokens2({ tokenAccounts: reserves.map(r => r.account.tokenInfo.tokenAccount.toString()) });
2612
}
13+
2714
async function borrowed(api) {
2815
const provider = getProvider()
2916
const program = new Program(idl, 'NxFiv1eeKtKT6dQEP2erBwz2DSKCTdb8WSsxVDwVGJ1', provider)
3017
const reserves = await program.account.reserve.all()
31-
reserves.map(r=>{
18+
reserves.map(r => {
3219
const amount = i80f48ToNumber(r.account.creditDebit.debtNtokenRatio) * i80f48ToNumber(r.account.creditDebit.reserveDebtNtokenAmount)
3320
const mint = r.account.tokenMint.toString()
3421
api.add(mint, amount)
@@ -37,5 +24,5 @@ async function borrowed(api) {
3724

3825
module.exports = {
3926
timetravel: false,
40-
solana: { tvl,borrowed },
27+
solana: { tvl, borrowed },
4128
}

0 commit comments

Comments
 (0)