@t2000/sdk 0.17.21 → 0.17.23

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 (254) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/cetus.d.ts +29 -0
  3. package/dist/adapters/cetus.d.ts.map +1 -0
  4. package/dist/adapters/cetus.js +74 -0
  5. package/dist/adapters/cetus.js.map +1 -0
  6. package/dist/adapters/cetus.test.d.ts +2 -0
  7. package/dist/adapters/cetus.test.d.ts.map +1 -0
  8. package/dist/adapters/cetus.test.js +57 -0
  9. package/dist/adapters/cetus.test.js.map +1 -0
  10. package/dist/adapters/compliance.test.d.ts +8 -0
  11. package/dist/adapters/compliance.test.d.ts.map +1 -0
  12. package/dist/adapters/compliance.test.js +202 -0
  13. package/dist/adapters/compliance.test.js.map +1 -0
  14. package/dist/adapters/index.d.ts +10 -4
  15. package/dist/adapters/index.d.ts.map +1 -0
  16. package/dist/adapters/index.js +15 -2107
  17. package/dist/adapters/index.js.map +1 -1
  18. package/dist/adapters/navi.d.ts +41 -0
  19. package/dist/adapters/navi.d.ts.map +1 -0
  20. package/dist/adapters/navi.js +102 -0
  21. package/dist/adapters/navi.js.map +1 -0
  22. package/dist/adapters/navi.test.d.ts +2 -0
  23. package/dist/adapters/navi.test.d.ts.map +1 -0
  24. package/dist/adapters/navi.test.js +164 -0
  25. package/dist/adapters/navi.test.js.map +1 -0
  26. package/dist/adapters/registry.d.ts +47 -0
  27. package/dist/adapters/registry.d.ts.map +1 -0
  28. package/dist/adapters/registry.js +162 -0
  29. package/dist/adapters/registry.js.map +1 -0
  30. package/dist/adapters/registry.test.d.ts +2 -0
  31. package/dist/adapters/registry.test.d.ts.map +1 -0
  32. package/dist/adapters/registry.test.js +197 -0
  33. package/dist/adapters/registry.test.js.map +1 -0
  34. package/dist/adapters/suilend.d.ts +71 -0
  35. package/dist/adapters/suilend.d.ts.map +1 -0
  36. package/dist/adapters/suilend.js +826 -0
  37. package/dist/adapters/suilend.js.map +1 -0
  38. package/dist/adapters/suilend.test.d.ts +2 -0
  39. package/dist/adapters/suilend.test.d.ts.map +1 -0
  40. package/dist/adapters/suilend.test.js +294 -0
  41. package/dist/adapters/suilend.test.js.map +1 -0
  42. package/dist/adapters/types.d.ts +160 -0
  43. package/dist/adapters/types.d.ts.map +1 -0
  44. package/dist/adapters/types.js +2 -0
  45. package/dist/adapters/types.js.map +1 -0
  46. package/dist/auto-invest.d.ts +23 -0
  47. package/dist/auto-invest.d.ts.map +1 -0
  48. package/dist/auto-invest.js +131 -0
  49. package/dist/auto-invest.js.map +1 -0
  50. package/dist/auto-invest.test.d.ts +2 -0
  51. package/dist/auto-invest.test.d.ts.map +1 -0
  52. package/dist/auto-invest.test.js +220 -0
  53. package/dist/auto-invest.test.js.map +1 -0
  54. package/dist/constants.d.ts +177 -0
  55. package/dist/constants.d.ts.map +1 -0
  56. package/dist/constants.js +135 -0
  57. package/dist/constants.js.map +1 -0
  58. package/dist/contacts.d.ts +25 -0
  59. package/dist/contacts.d.ts.map +1 -0
  60. package/dist/contacts.js +83 -0
  61. package/dist/contacts.js.map +1 -0
  62. package/dist/contacts.test.d.ts +2 -0
  63. package/dist/contacts.test.d.ts.map +1 -0
  64. package/dist/contacts.test.js +164 -0
  65. package/dist/contacts.test.js.map +1 -0
  66. package/dist/errors.d.ts +26 -0
  67. package/dist/errors.d.ts.map +1 -0
  68. package/dist/errors.js +81 -0
  69. package/dist/errors.js.map +1 -0
  70. package/dist/errors.test.d.ts +2 -0
  71. package/dist/errors.test.d.ts.map +1 -0
  72. package/dist/errors.test.js +48 -0
  73. package/dist/errors.test.js.map +1 -0
  74. package/dist/gas/autoTopUp.d.ts +18 -0
  75. package/dist/gas/autoTopUp.d.ts.map +1 -0
  76. package/dist/gas/autoTopUp.js +55 -0
  77. package/dist/gas/autoTopUp.js.map +1 -0
  78. package/dist/gas/autoTopUp.test.d.ts +2 -0
  79. package/dist/gas/autoTopUp.test.d.ts.map +1 -0
  80. package/dist/gas/autoTopUp.test.js +59 -0
  81. package/dist/gas/autoTopUp.test.js.map +1 -0
  82. package/dist/gas/gasStation.d.ts +23 -0
  83. package/dist/gas/gasStation.d.ts.map +1 -0
  84. package/dist/gas/gasStation.js +58 -0
  85. package/dist/gas/gasStation.js.map +1 -0
  86. package/dist/gas/index.d.ts +4 -0
  87. package/dist/gas/index.d.ts.map +1 -0
  88. package/dist/gas/index.js +4 -0
  89. package/dist/gas/index.js.map +1 -0
  90. package/dist/gas/manager.d.ts +24 -0
  91. package/dist/gas/manager.d.ts.map +1 -0
  92. package/dist/gas/manager.js +142 -0
  93. package/dist/gas/manager.js.map +1 -0
  94. package/dist/gas/manager.test.d.ts +2 -0
  95. package/dist/gas/manager.test.d.ts.map +1 -0
  96. package/dist/gas/manager.test.js +220 -0
  97. package/dist/gas/manager.test.js.map +1 -0
  98. package/dist/gas/serialization.test.d.ts +2 -0
  99. package/dist/gas/serialization.test.d.ts.map +1 -0
  100. package/dist/gas/serialization.test.js +47 -0
  101. package/dist/gas/serialization.test.js.map +1 -0
  102. package/dist/index.d.ts +30 -649
  103. package/dist/index.d.ts.map +1 -0
  104. package/dist/index.js +21 -6053
  105. package/dist/index.js.map +1 -1
  106. package/dist/invest.test.d.ts +2 -0
  107. package/dist/invest.test.d.ts.map +1 -0
  108. package/dist/invest.test.js +256 -0
  109. package/dist/invest.test.js.map +1 -0
  110. package/dist/portfolio.d.ts +39 -0
  111. package/dist/portfolio.d.ts.map +1 -0
  112. package/dist/portfolio.js +201 -0
  113. package/dist/portfolio.js.map +1 -0
  114. package/dist/portfolio.test.d.ts +2 -0
  115. package/dist/portfolio.test.d.ts.map +1 -0
  116. package/dist/portfolio.test.js +301 -0
  117. package/dist/portfolio.test.js.map +1 -0
  118. package/dist/protocols/cetus.d.ts +73 -0
  119. package/dist/protocols/cetus.d.ts.map +1 -0
  120. package/dist/protocols/cetus.js +267 -0
  121. package/dist/protocols/cetus.js.map +1 -0
  122. package/dist/protocols/cetus.test.d.ts +2 -0
  123. package/dist/protocols/cetus.test.d.ts.map +1 -0
  124. package/dist/protocols/cetus.test.js +325 -0
  125. package/dist/protocols/cetus.test.js.map +1 -0
  126. package/dist/protocols/navi.d.ts +59 -0
  127. package/dist/protocols/navi.d.ts.map +1 -0
  128. package/dist/protocols/navi.js +945 -0
  129. package/dist/protocols/navi.js.map +1 -0
  130. package/dist/protocols/navi.test.d.ts +2 -0
  131. package/dist/protocols/navi.test.d.ts.map +1 -0
  132. package/dist/protocols/navi.test.js +339 -0
  133. package/dist/protocols/navi.test.js.map +1 -0
  134. package/dist/protocols/protocolFee.d.ts +17 -0
  135. package/dist/protocols/protocolFee.d.ts.map +1 -0
  136. package/dist/protocols/protocolFee.js +62 -0
  137. package/dist/protocols/protocolFee.js.map +1 -0
  138. package/dist/protocols/protocolFee.test.d.ts +2 -0
  139. package/dist/protocols/protocolFee.test.d.ts.map +1 -0
  140. package/dist/protocols/protocolFee.test.js +137 -0
  141. package/dist/protocols/protocolFee.test.js.map +1 -0
  142. package/dist/protocols/sentinel.d.ts +18 -0
  143. package/dist/protocols/sentinel.d.ts.map +1 -0
  144. package/dist/protocols/sentinel.js +188 -0
  145. package/dist/protocols/sentinel.js.map +1 -0
  146. package/dist/protocols/sentinel.test.d.ts +2 -0
  147. package/dist/protocols/sentinel.test.d.ts.map +1 -0
  148. package/dist/protocols/sentinel.test.js +199 -0
  149. package/dist/protocols/sentinel.test.js.map +1 -0
  150. package/dist/protocols/yieldTracker.d.ts +6 -0
  151. package/dist/protocols/yieldTracker.d.ts.map +1 -0
  152. package/dist/protocols/yieldTracker.js +29 -0
  153. package/dist/protocols/yieldTracker.js.map +1 -0
  154. package/dist/safeguards/enforcer.d.ts +18 -0
  155. package/dist/safeguards/enforcer.d.ts.map +1 -0
  156. package/dist/safeguards/enforcer.js +130 -0
  157. package/dist/safeguards/enforcer.js.map +1 -0
  158. package/dist/safeguards/enforcer.test.d.ts +2 -0
  159. package/dist/safeguards/enforcer.test.d.ts.map +1 -0
  160. package/dist/safeguards/enforcer.test.js +212 -0
  161. package/dist/safeguards/enforcer.test.js.map +1 -0
  162. package/dist/safeguards/errors.d.ts +24 -0
  163. package/dist/safeguards/errors.d.ts.map +1 -0
  164. package/dist/safeguards/errors.js +31 -0
  165. package/dist/safeguards/errors.js.map +1 -0
  166. package/dist/safeguards/index.d.ts +6 -0
  167. package/dist/safeguards/index.d.ts.map +1 -0
  168. package/dist/safeguards/index.js +4 -0
  169. package/dist/safeguards/index.js.map +1 -0
  170. package/dist/safeguards/types.d.ts +16 -0
  171. package/dist/safeguards/types.d.ts.map +1 -0
  172. package/dist/safeguards/types.js +13 -0
  173. package/dist/safeguards/types.js.map +1 -0
  174. package/dist/strategy.d.ts +22 -0
  175. package/dist/strategy.d.ts.map +1 -0
  176. package/dist/strategy.js +113 -0
  177. package/dist/strategy.js.map +1 -0
  178. package/dist/strategy.test.d.ts +2 -0
  179. package/dist/strategy.test.d.ts.map +1 -0
  180. package/dist/strategy.test.js +212 -0
  181. package/dist/strategy.test.js.map +1 -0
  182. package/dist/t2000.d.ts +218 -0
  183. package/dist/t2000.d.ts.map +1 -0
  184. package/dist/t2000.integration.test.d.ts +2 -0
  185. package/dist/t2000.integration.test.d.ts.map +1 -0
  186. package/dist/t2000.integration.test.js +954 -0
  187. package/dist/t2000.integration.test.js.map +1 -0
  188. package/dist/t2000.js +2395 -0
  189. package/dist/t2000.js.map +1 -0
  190. package/dist/types.d.ts +419 -0
  191. package/dist/types.d.ts.map +1 -0
  192. package/dist/types.js +2 -0
  193. package/dist/types.js.map +1 -0
  194. package/dist/utils/format.d.ts +22 -0
  195. package/dist/utils/format.d.ts.map +1 -0
  196. package/dist/utils/format.js +71 -0
  197. package/dist/utils/format.js.map +1 -0
  198. package/dist/utils/format.test.d.ts +2 -0
  199. package/dist/utils/format.test.d.ts.map +1 -0
  200. package/dist/utils/format.test.js +187 -0
  201. package/dist/utils/format.test.js.map +1 -0
  202. package/dist/utils/hashcash.d.ts +2 -0
  203. package/dist/utils/hashcash.d.ts.map +1 -0
  204. package/dist/utils/hashcash.js +27 -0
  205. package/dist/utils/hashcash.js.map +1 -0
  206. package/dist/utils/hashcash.test.d.ts +2 -0
  207. package/dist/utils/hashcash.test.d.ts.map +1 -0
  208. package/dist/utils/hashcash.test.js +40 -0
  209. package/dist/utils/hashcash.test.js.map +1 -0
  210. package/dist/utils/retry.d.ts +9 -0
  211. package/dist/utils/retry.d.ts.map +1 -0
  212. package/dist/utils/retry.js +47 -0
  213. package/dist/utils/retry.js.map +1 -0
  214. package/dist/utils/simulate.d.ts +15 -0
  215. package/dist/utils/simulate.d.ts.map +1 -0
  216. package/dist/utils/simulate.js +75 -0
  217. package/dist/utils/simulate.js.map +1 -0
  218. package/dist/utils/simulate.test.d.ts +2 -0
  219. package/dist/utils/simulate.test.d.ts.map +1 -0
  220. package/dist/utils/simulate.test.js +80 -0
  221. package/dist/utils/simulate.test.js.map +1 -0
  222. package/dist/utils/sui.d.ts +6 -0
  223. package/dist/utils/sui.d.ts.map +1 -0
  224. package/dist/utils/sui.js +28 -0
  225. package/dist/utils/sui.js.map +1 -0
  226. package/dist/utils/sui.test.d.ts +2 -0
  227. package/dist/utils/sui.test.d.ts.map +1 -0
  228. package/dist/utils/sui.test.js +58 -0
  229. package/dist/utils/sui.test.js.map +1 -0
  230. package/dist/wallet/balance.d.ts +4 -0
  231. package/dist/wallet/balance.d.ts.map +1 -0
  232. package/dist/wallet/balance.js +98 -0
  233. package/dist/wallet/balance.js.map +1 -0
  234. package/dist/wallet/history.d.ts +4 -0
  235. package/dist/wallet/history.d.ts.map +1 -0
  236. package/dist/wallet/history.js +38 -0
  237. package/dist/wallet/history.js.map +1 -0
  238. package/dist/wallet/keyManager.d.ts +9 -0
  239. package/dist/wallet/keyManager.d.ts.map +1 -0
  240. package/dist/wallet/keyManager.js +113 -0
  241. package/dist/wallet/keyManager.js.map +1 -0
  242. package/dist/wallet/keyManager.test.d.ts +2 -0
  243. package/dist/wallet/keyManager.test.d.ts.map +1 -0
  244. package/dist/wallet/keyManager.test.js +55 -0
  245. package/dist/wallet/keyManager.test.js.map +1 -0
  246. package/dist/wallet/send.d.ts +24 -0
  247. package/dist/wallet/send.d.ts.map +1 -0
  248. package/dist/wallet/send.js +95 -0
  249. package/dist/wallet/send.js.map +1 -0
  250. package/dist/wallet/send.test.d.ts +2 -0
  251. package/dist/wallet/send.test.d.ts.map +1 -0
  252. package/dist/wallet/send.test.js +69 -0
  253. package/dist/wallet/send.test.js.map +1 -0
  254. package/package.json +1 -1
@@ -0,0 +1,945 @@
1
+ import { Transaction } from '@mysten/sui/transactions';
2
+ import { bcs } from '@mysten/sui/bcs';
3
+ import { SUPPORTED_ASSETS, STABLE_ASSETS } from '../constants.js';
4
+ import { T2000Error } from '../errors.js';
5
+ import { stableToRaw } from '../utils/format.js';
6
+ import { addCollectFeeToTx } from './protocolFee.js';
7
+ const USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
8
+ const RATE_DECIMALS = 27;
9
+ const LTV_DECIMALS = 27;
10
+ const MIN_HEALTH_FACTOR = 1.5;
11
+ function withdrawDustBuffer(decimals) {
12
+ return 1000 / 10 ** decimals;
13
+ }
14
+ const CLOCK = '0x06';
15
+ const SUI_SYSTEM_STATE = '0x05';
16
+ const NAVI_BALANCE_DECIMALS = 9;
17
+ const CONFIG_API = 'https://open-api.naviprotocol.io/api/navi/config?env=prod';
18
+ const POOLS_API = 'https://open-api.naviprotocol.io/api/navi/pools?env=prod';
19
+ const PACKAGE_API = 'https://open-api.naviprotocol.io/api/package';
20
+ let packageCache = null;
21
+ function toBigInt(v) {
22
+ if (typeof v === 'bigint')
23
+ return v;
24
+ return BigInt(String(v));
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // BCS
28
+ // ---------------------------------------------------------------------------
29
+ const UserStateInfo = bcs.struct('UserStateInfo', {
30
+ asset_id: bcs.u8(),
31
+ borrow_balance: bcs.u256(),
32
+ supply_balance: bcs.u256(),
33
+ });
34
+ function decodeDevInspect(result, schema) {
35
+ const rv = result.results?.[0]?.returnValues?.[0];
36
+ if (result.error || !rv)
37
+ return undefined;
38
+ const bytes = Uint8Array.from(rv[0]);
39
+ return schema.parse(bytes);
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Config + Pool cache
43
+ // ---------------------------------------------------------------------------
44
+ let configCache = null;
45
+ let poolsCache = null;
46
+ const CACHE_TTL = 5 * 60_000;
47
+ async function fetchJson(url) {
48
+ const res = await fetch(url);
49
+ if (!res.ok)
50
+ throw new T2000Error('PROTOCOL_UNAVAILABLE', `NAVI API error: ${res.status}`);
51
+ const json = (await res.json());
52
+ return (json.data ?? json);
53
+ }
54
+ async function getLatestPackageId() {
55
+ if (packageCache && Date.now() - packageCache.ts < CACHE_TTL)
56
+ return packageCache.id;
57
+ const res = await fetch(PACKAGE_API);
58
+ if (!res.ok)
59
+ throw new T2000Error('PROTOCOL_UNAVAILABLE', `NAVI package API error: ${res.status}`);
60
+ const json = (await res.json());
61
+ if (!json.packageId)
62
+ throw new T2000Error('PROTOCOL_UNAVAILABLE', 'NAVI package API returned no packageId');
63
+ packageCache = { id: json.packageId, ts: Date.now() };
64
+ return json.packageId;
65
+ }
66
+ async function getConfig(fresh = false) {
67
+ if (configCache && !fresh && Date.now() - configCache.ts < CACHE_TTL)
68
+ return configCache.data;
69
+ const [data, latestPkg] = await Promise.all([
70
+ fetchJson(CONFIG_API),
71
+ getLatestPackageId(),
72
+ ]);
73
+ data.package = latestPkg;
74
+ configCache = { data, ts: Date.now() };
75
+ return data;
76
+ }
77
+ async function getPools(fresh = false) {
78
+ if (poolsCache && !fresh && Date.now() - poolsCache.ts < CACHE_TTL)
79
+ return poolsCache.data;
80
+ const data = await fetchJson(POOLS_API);
81
+ poolsCache = { data, ts: Date.now() };
82
+ return data;
83
+ }
84
+ function matchesCoinType(poolType, targetType) {
85
+ const poolSuffix = poolType.split('::').slice(1).join('::').toLowerCase();
86
+ const targetSuffix = targetType.split('::').slice(1).join('::').toLowerCase();
87
+ return poolSuffix === targetSuffix;
88
+ }
89
+ function resolvePoolSymbol(pool) {
90
+ const coinType = pool.suiCoinType || pool.coinType || '';
91
+ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
92
+ if (matchesCoinType(coinType, info.type))
93
+ return key;
94
+ }
95
+ return pool.token?.symbol ?? 'UNKNOWN';
96
+ }
97
+ function resolveAssetInfo(asset) {
98
+ if (asset in SUPPORTED_ASSETS) {
99
+ const info = SUPPORTED_ASSETS[asset];
100
+ return { type: info.type, decimals: info.decimals, displayName: info.displayName };
101
+ }
102
+ throw new T2000Error('ASSET_NOT_SUPPORTED', `Unknown asset: ${asset}`);
103
+ }
104
+ async function getPool(asset = 'USDC') {
105
+ const pools = await getPools();
106
+ const { type: targetType, displayName } = resolveAssetInfo(asset);
107
+ const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || '', targetType));
108
+ if (!pool) {
109
+ throw new T2000Error('ASSET_NOT_SUPPORTED', `${displayName} pool not found on NAVI`);
110
+ }
111
+ return pool;
112
+ }
113
+ async function getUsdcPool() {
114
+ return getPool('USDC');
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Oracle price update (required before withdraw/borrow)
118
+ // ---------------------------------------------------------------------------
119
+ function addOracleUpdate(tx, config, pool) {
120
+ const feed = config.oracle.feeds?.find((f) => f.assetId === pool.id);
121
+ if (!feed) {
122
+ throw new T2000Error('PROTOCOL_UNAVAILABLE', `Oracle feed not found for asset ${pool.token?.symbol ?? pool.id}`);
123
+ }
124
+ tx.moveCall({
125
+ target: `${config.oracle.packageId}::oracle_pro::update_single_price_v2`,
126
+ arguments: [
127
+ tx.object(CLOCK),
128
+ tx.object(config.oracle.oracleConfig),
129
+ tx.object(config.oracle.priceOracle),
130
+ tx.object(config.oracle.supraOracleHolder),
131
+ tx.object(feed.pythPriceInfoObject),
132
+ tx.object(config.oracle.switchboardAggregator),
133
+ tx.pure.address(feed.feedId),
134
+ ],
135
+ });
136
+ }
137
+ /**
138
+ * Updates NAVI oracles for all supported assets that have active positions.
139
+ * Adds on-chain oracle refresh commands to the PTB so NAVI reads fresh
140
+ * price data maintained by keeper bots.
141
+ *
142
+ * For stablecoin-only operations, pass `assetFilter` to limit to stables.
143
+ * For investment assets (SUI/ETH), the full set is refreshed.
144
+ */
145
+ function refreshOracles(tx, config, pools, opts) {
146
+ const assetsToRefresh = opts?.assetsToRefresh ?? NAVI_SUPPORTED_ASSETS;
147
+ const targetTypes = assetsToRefresh.map((a) => SUPPORTED_ASSETS[a].type);
148
+ const matchedPools = pools.filter((p) => {
149
+ const ct = p.suiCoinType || p.coinType || '';
150
+ return targetTypes.some((t) => matchesCoinType(ct, t));
151
+ });
152
+ for (const pool of matchedPools) {
153
+ addOracleUpdate(tx, config, pool);
154
+ }
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Helpers
158
+ // ---------------------------------------------------------------------------
159
+ function extractGasCost(effects) {
160
+ if (!effects?.gasUsed)
161
+ return 0;
162
+ return Math.abs((Number(effects.gasUsed.computationCost) +
163
+ Number(effects.gasUsed.storageCost) -
164
+ Number(effects.gasUsed.storageRebate)) / 1e9);
165
+ }
166
+ function rateToApy(rawRate) {
167
+ if (!rawRate || rawRate === '0')
168
+ return 0;
169
+ return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
170
+ }
171
+ function poolSaveApy(pool) {
172
+ const incentive = parseFloat(pool.supplyIncentiveApyInfo?.apy ?? '0');
173
+ if (incentive > 0)
174
+ return incentive;
175
+ return rateToApy(pool.currentSupplyRate);
176
+ }
177
+ function poolBorrowApy(pool) {
178
+ const incentive = parseFloat(pool.borrowIncentiveApyInfo?.apy ?? '0');
179
+ if (incentive > 0)
180
+ return incentive;
181
+ return rateToApy(pool.currentBorrowRate);
182
+ }
183
+ function parseLtv(rawLtv) {
184
+ if (!rawLtv || rawLtv === '0')
185
+ return 0.75;
186
+ return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
187
+ }
188
+ function parseLiqThreshold(val) {
189
+ if (typeof val === 'number')
190
+ return val;
191
+ const n = Number(val);
192
+ if (n > 1)
193
+ return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
194
+ return n;
195
+ }
196
+ function normalizeHealthFactor(raw) {
197
+ const v = raw / 10 ** RATE_DECIMALS;
198
+ return v > 1e5 ? Infinity : v;
199
+ }
200
+ function naviStorageDecimals(poolId, tokenDecimals) {
201
+ // Original NAVI pools (id 0-10) normalize all balances to 9 decimals.
202
+ // Newer pools (id >= 11, e.g. suiETH, suiUSDT) use native token decimals.
203
+ if (poolId <= 10)
204
+ return NAVI_BALANCE_DECIMALS;
205
+ return tokenDecimals;
206
+ }
207
+ function compoundBalance(rawBalance, currentIndex, pool) {
208
+ if (!rawBalance || !currentIndex || currentIndex === '0')
209
+ return 0;
210
+ const scale = BigInt('1' + '0'.repeat(RATE_DECIMALS));
211
+ const half = scale / 2n;
212
+ const result = (rawBalance * BigInt(currentIndex) + half) / scale;
213
+ const decimals = pool ? naviStorageDecimals(pool.id, pool.token.decimals) : NAVI_BALANCE_DECIMALS;
214
+ return Number(result) / 10 ** decimals;
215
+ }
216
+ // ---------------------------------------------------------------------------
217
+ // On-chain reads
218
+ // ---------------------------------------------------------------------------
219
+ async function getUserState(client, address) {
220
+ const config = await getConfig();
221
+ const tx = new Transaction();
222
+ tx.moveCall({
223
+ target: `${config.uiGetter}::getter_unchecked::get_user_state`,
224
+ arguments: [tx.object(config.storage), tx.pure.address(address)],
225
+ });
226
+ const result = await client.devInspectTransactionBlock({
227
+ transactionBlock: tx,
228
+ sender: address,
229
+ });
230
+ const decoded = decodeDevInspect(result, bcs.vector(UserStateInfo));
231
+ if (!decoded)
232
+ return [];
233
+ const mapped = decoded
234
+ .map((s) => ({
235
+ assetId: s.asset_id,
236
+ supplyBalance: toBigInt(s.supply_balance),
237
+ borrowBalance: toBigInt(s.borrow_balance),
238
+ }));
239
+ return mapped.filter((s) => s.supplyBalance !== 0n || s.borrowBalance !== 0n);
240
+ }
241
+ async function fetchCoins(client, owner, coinType) {
242
+ const all = [];
243
+ let cursor;
244
+ let hasNext = true;
245
+ while (hasNext) {
246
+ const page = await client.getCoins({ owner, coinType, cursor: cursor ?? undefined });
247
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
248
+ cursor = page.nextCursor;
249
+ hasNext = page.hasNextPage;
250
+ }
251
+ return all;
252
+ }
253
+ function mergeCoins(tx, coins) {
254
+ if (coins.length === 0)
255
+ throw new T2000Error('INSUFFICIENT_BALANCE', 'No coins to merge');
256
+ const primary = tx.object(coins[0].coinObjectId);
257
+ if (coins.length > 1) {
258
+ tx.mergeCoins(primary, coins.slice(1).map((c) => tx.object(c.coinObjectId)));
259
+ }
260
+ return primary;
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // Public API
264
+ // ---------------------------------------------------------------------------
265
+ export async function buildSaveTx(client, address, amount, options = {}) {
266
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
267
+ throw new T2000Error('INVALID_AMOUNT', 'Save amount must be a positive number');
268
+ }
269
+ const asset = options.asset ?? 'USDC';
270
+ const assetInfo = resolveAssetInfo(asset);
271
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
272
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
273
+ const coins = await fetchCoins(client, address, assetInfo.type);
274
+ if (coins.length === 0)
275
+ throw new T2000Error('INSUFFICIENT_BALANCE', `No ${assetInfo.displayName} coins found`);
276
+ const tx = new Transaction();
277
+ tx.setSender(address);
278
+ const coinObj = mergeCoins(tx, coins);
279
+ if (options.collectFee) {
280
+ addCollectFeeToTx(tx, coinObj, 'save');
281
+ }
282
+ tx.moveCall({
283
+ target: `${config.package}::incentive_v3::entry_deposit`,
284
+ arguments: [
285
+ tx.object(CLOCK),
286
+ tx.object(config.storage),
287
+ tx.object(pool.contract.pool),
288
+ tx.pure.u8(pool.id),
289
+ coinObj,
290
+ tx.pure.u64(rawAmount),
291
+ tx.object(config.incentiveV2),
292
+ tx.object(config.incentiveV3),
293
+ ],
294
+ typeArguments: [pool.suiCoinType],
295
+ });
296
+ return tx;
297
+ }
298
+ export async function save(client, keypair, amount) {
299
+ const address = keypair.getPublicKey().toSuiAddress();
300
+ const tx = await buildSaveTx(client, address, amount);
301
+ const result = await client.signAndExecuteTransaction({
302
+ signer: keypair,
303
+ transaction: tx,
304
+ options: { showEffects: true },
305
+ });
306
+ await client.waitForTransaction({ digest: result.digest });
307
+ const rates = await getRates(client);
308
+ return {
309
+ success: true,
310
+ tx: result.digest,
311
+ amount,
312
+ apy: rates.USDC.saveApy,
313
+ fee: 0,
314
+ gasCost: extractGasCost(result.effects),
315
+ gasMethod: 'self-funded',
316
+ savingsBalance: amount,
317
+ };
318
+ }
319
+ export async function buildWithdrawTx(client, address, amount, options = {}) {
320
+ const asset = options.asset ?? 'USDC';
321
+ const assetInfo = resolveAssetInfo(asset);
322
+ const [config, pool, pools, states] = await Promise.all([
323
+ getConfig(),
324
+ getPool(asset),
325
+ getPools(),
326
+ getUserState(client, address),
327
+ ]);
328
+ const assetState = states.find((s) => s.assetId === pool.id);
329
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
330
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
331
+ if (effectiveAmount <= 0)
332
+ throw new T2000Error('NO_COLLATERAL', `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
333
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
334
+ if (rawAmount <= 0) {
335
+ throw new T2000Error('INVALID_AMOUNT', `Withdrawal amount rounds to zero — balance is dust`);
336
+ }
337
+ const tx = new Transaction();
338
+ tx.setSender(address);
339
+ refreshOracles(tx, config, pools);
340
+ const [balance] = tx.moveCall({
341
+ target: `${config.package}::incentive_v3::withdraw_v2`,
342
+ arguments: [
343
+ tx.object(CLOCK),
344
+ tx.object(config.oracle.priceOracle),
345
+ tx.object(config.storage),
346
+ tx.object(pool.contract.pool),
347
+ tx.pure.u8(pool.id),
348
+ tx.pure.u64(rawAmount),
349
+ tx.object(config.incentiveV2),
350
+ tx.object(config.incentiveV3),
351
+ tx.object(SUI_SYSTEM_STATE),
352
+ ],
353
+ typeArguments: [pool.suiCoinType],
354
+ });
355
+ const [coin] = tx.moveCall({
356
+ target: '0x2::coin::from_balance',
357
+ arguments: [balance],
358
+ typeArguments: [pool.suiCoinType],
359
+ });
360
+ tx.transferObjects([coin], address);
361
+ return { tx, effectiveAmount };
362
+ }
363
+ /**
364
+ * Composable variant: adds withdraw commands to an existing PTB and
365
+ * returns the coin object for chaining (no transferObjects).
366
+ */
367
+ export async function addWithdrawToTx(tx, client, address, amount, options = {}) {
368
+ const asset = options.asset ?? 'USDC';
369
+ const assetInfo = resolveAssetInfo(asset);
370
+ const [config, pool, pools, states] = await Promise.all([
371
+ getConfig(),
372
+ getPool(asset),
373
+ getPools(),
374
+ getUserState(client, address),
375
+ ]);
376
+ const assetState = states.find((s) => s.assetId === pool.id);
377
+ const deposited = assetState ? compoundBalance(assetState.supplyBalance, pool.currentSupplyIndex, pool) : 0;
378
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - withdrawDustBuffer(assetInfo.decimals)));
379
+ if (effectiveAmount <= 0)
380
+ throw new T2000Error('NO_COLLATERAL', `Nothing to withdraw for ${assetInfo.displayName} on NAVI`);
381
+ const rawAmount = Number(stableToRaw(effectiveAmount, assetInfo.decimals));
382
+ if (rawAmount <= 0) {
383
+ // Dust position — create a zero-value coin instead of calling on-chain withdraw
384
+ const [coin] = tx.moveCall({
385
+ target: '0x2::coin::zero',
386
+ typeArguments: [pool.suiCoinType],
387
+ });
388
+ return { coin, effectiveAmount: 0 };
389
+ }
390
+ refreshOracles(tx, config, pools);
391
+ const [balance] = tx.moveCall({
392
+ target: `${config.package}::incentive_v3::withdraw_v2`,
393
+ arguments: [
394
+ tx.object(CLOCK),
395
+ tx.object(config.oracle.priceOracle),
396
+ tx.object(config.storage),
397
+ tx.object(pool.contract.pool),
398
+ tx.pure.u8(pool.id),
399
+ tx.pure.u64(rawAmount),
400
+ tx.object(config.incentiveV2),
401
+ tx.object(config.incentiveV3),
402
+ tx.object(SUI_SYSTEM_STATE),
403
+ ],
404
+ typeArguments: [pool.suiCoinType],
405
+ });
406
+ const [coin] = tx.moveCall({
407
+ target: '0x2::coin::from_balance',
408
+ arguments: [balance],
409
+ typeArguments: [pool.suiCoinType],
410
+ });
411
+ return { coin, effectiveAmount };
412
+ }
413
+ /**
414
+ * Composable variant: adds deposit commands to an existing PTB
415
+ * using a coin object from a prior step (withdraw/swap).
416
+ */
417
+ export async function addSaveToTx(tx, _client, _address, coin, options = {}) {
418
+ const asset = options.asset ?? 'USDC';
419
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
420
+ if (options.collectFee) {
421
+ addCollectFeeToTx(tx, coin, 'save');
422
+ }
423
+ const [coinValue] = tx.moveCall({
424
+ target: '0x2::coin::value',
425
+ typeArguments: [pool.suiCoinType],
426
+ arguments: [coin],
427
+ });
428
+ tx.moveCall({
429
+ target: `${config.package}::incentive_v3::entry_deposit`,
430
+ arguments: [
431
+ tx.object(CLOCK),
432
+ tx.object(config.storage),
433
+ tx.object(pool.contract.pool),
434
+ tx.pure.u8(pool.id),
435
+ coin,
436
+ coinValue,
437
+ tx.object(config.incentiveV2),
438
+ tx.object(config.incentiveV3),
439
+ ],
440
+ typeArguments: [pool.suiCoinType],
441
+ });
442
+ }
443
+ /**
444
+ * Composable variant: adds repay commands to an existing PTB
445
+ * using a coin object from a prior step (swap).
446
+ */
447
+ export async function addRepayToTx(tx, _client, _address, coin, options = {}) {
448
+ const asset = options.asset ?? 'USDC';
449
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
450
+ addOracleUpdate(tx, config, pool);
451
+ const [coinValue] = tx.moveCall({
452
+ target: '0x2::coin::value',
453
+ typeArguments: [pool.suiCoinType],
454
+ arguments: [coin],
455
+ });
456
+ tx.moveCall({
457
+ target: `${config.package}::incentive_v3::entry_repay`,
458
+ arguments: [
459
+ tx.object(CLOCK),
460
+ tx.object(config.oracle.priceOracle),
461
+ tx.object(config.storage),
462
+ tx.object(pool.contract.pool),
463
+ tx.pure.u8(pool.id),
464
+ coin,
465
+ coinValue,
466
+ tx.object(config.incentiveV2),
467
+ tx.object(config.incentiveV3),
468
+ ],
469
+ typeArguments: [pool.suiCoinType],
470
+ });
471
+ }
472
+ export async function withdraw(client, keypair, amount) {
473
+ const address = keypair.getPublicKey().toSuiAddress();
474
+ const { tx, effectiveAmount } = await buildWithdrawTx(client, address, amount);
475
+ const result = await client.signAndExecuteTransaction({
476
+ signer: keypair,
477
+ transaction: tx,
478
+ options: { showEffects: true },
479
+ });
480
+ await client.waitForTransaction({ digest: result.digest });
481
+ return {
482
+ success: true,
483
+ tx: result.digest,
484
+ amount: effectiveAmount,
485
+ gasCost: extractGasCost(result.effects),
486
+ gasMethod: 'self-funded',
487
+ };
488
+ }
489
+ export async function buildBorrowTx(client, address, amount, options = {}) {
490
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
491
+ throw new T2000Error('INVALID_AMOUNT', 'Borrow amount must be a positive number');
492
+ }
493
+ const asset = options.asset ?? 'USDC';
494
+ const assetInfo = resolveAssetInfo(asset);
495
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
496
+ const [config, pool, pools] = await Promise.all([
497
+ getConfig(), getPool(asset), getPools(),
498
+ ]);
499
+ const tx = new Transaction();
500
+ tx.setSender(address);
501
+ refreshOracles(tx, config, pools);
502
+ const [balance] = tx.moveCall({
503
+ target: `${config.package}::incentive_v3::borrow_v2`,
504
+ arguments: [
505
+ tx.object(CLOCK),
506
+ tx.object(config.oracle.priceOracle),
507
+ tx.object(config.storage),
508
+ tx.object(pool.contract.pool),
509
+ tx.pure.u8(pool.id),
510
+ tx.pure.u64(rawAmount),
511
+ tx.object(config.incentiveV2),
512
+ tx.object(config.incentiveV3),
513
+ tx.object(SUI_SYSTEM_STATE),
514
+ ],
515
+ typeArguments: [pool.suiCoinType],
516
+ });
517
+ const [borrowedCoin] = tx.moveCall({
518
+ target: '0x2::coin::from_balance',
519
+ arguments: [balance],
520
+ typeArguments: [pool.suiCoinType],
521
+ });
522
+ if (options.collectFee) {
523
+ addCollectFeeToTx(tx, borrowedCoin, 'borrow');
524
+ }
525
+ tx.transferObjects([borrowedCoin], address);
526
+ return tx;
527
+ }
528
+ export async function borrow(client, keypair, amount) {
529
+ const address = keypair.getPublicKey().toSuiAddress();
530
+ const tx = await buildBorrowTx(client, address, amount);
531
+ const result = await client.signAndExecuteTransaction({
532
+ signer: keypair,
533
+ transaction: tx,
534
+ options: { showEffects: true },
535
+ });
536
+ await client.waitForTransaction({ digest: result.digest });
537
+ const hfResult = await getHealthFactor(client, address);
538
+ return {
539
+ success: true,
540
+ tx: result.digest,
541
+ amount,
542
+ fee: 0,
543
+ healthFactor: hfResult.healthFactor,
544
+ gasCost: extractGasCost(result.effects),
545
+ gasMethod: 'self-funded',
546
+ };
547
+ }
548
+ export async function buildRepayTx(client, address, amount, options = {}) {
549
+ if (!amount || amount <= 0 || !Number.isFinite(amount)) {
550
+ throw new T2000Error('INVALID_AMOUNT', 'Repay amount must be a positive number');
551
+ }
552
+ const asset = options.asset ?? 'USDC';
553
+ const assetInfo = resolveAssetInfo(asset);
554
+ const rawAmount = Number(stableToRaw(amount, assetInfo.decimals));
555
+ const [config, pool] = await Promise.all([getConfig(), getPool(asset)]);
556
+ const coins = await fetchCoins(client, address, assetInfo.type);
557
+ if (coins.length === 0)
558
+ throw new T2000Error('INSUFFICIENT_BALANCE', `No ${assetInfo.displayName} coins to repay with`);
559
+ const tx = new Transaction();
560
+ tx.setSender(address);
561
+ addOracleUpdate(tx, config, pool);
562
+ const coinObj = mergeCoins(tx, coins);
563
+ tx.moveCall({
564
+ target: `${config.package}::incentive_v3::entry_repay`,
565
+ arguments: [
566
+ tx.object(CLOCK),
567
+ tx.object(config.oracle.priceOracle),
568
+ tx.object(config.storage),
569
+ tx.object(pool.contract.pool),
570
+ tx.pure.u8(pool.id),
571
+ coinObj,
572
+ tx.pure.u64(rawAmount),
573
+ tx.object(config.incentiveV2),
574
+ tx.object(config.incentiveV3),
575
+ ],
576
+ typeArguments: [pool.suiCoinType],
577
+ });
578
+ return tx;
579
+ }
580
+ export async function repay(client, keypair, amount) {
581
+ const address = keypair.getPublicKey().toSuiAddress();
582
+ const tx = await buildRepayTx(client, address, amount);
583
+ const result = await client.signAndExecuteTransaction({
584
+ signer: keypair,
585
+ transaction: tx,
586
+ options: { showEffects: true },
587
+ });
588
+ await client.waitForTransaction({ digest: result.digest });
589
+ const states = await getUserState(client, address);
590
+ const pools = await getPools();
591
+ let remainingDebt = 0;
592
+ for (const state of states) {
593
+ const pool = pools.find((p) => p.id === state.assetId);
594
+ if (!pool)
595
+ continue;
596
+ remainingDebt += compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
597
+ }
598
+ return {
599
+ success: true,
600
+ tx: result.digest,
601
+ amount,
602
+ remainingDebt,
603
+ gasCost: extractGasCost(result.effects),
604
+ gasMethod: 'self-funded',
605
+ };
606
+ }
607
+ export async function getHealthFactor(client, addressOrKeypair) {
608
+ const address = typeof addressOrKeypair === 'string'
609
+ ? addressOrKeypair
610
+ : addressOrKeypair.getPublicKey().toSuiAddress();
611
+ const [config, pools, states] = await Promise.all([
612
+ getConfig(),
613
+ getPools(),
614
+ getUserState(client, address),
615
+ ]);
616
+ let supplied = 0;
617
+ let borrowed = 0;
618
+ let weightedLtv = 0;
619
+ let weightedLiqThreshold = 0;
620
+ for (const state of states) {
621
+ const pool = pools.find((p) => p.id === state.assetId);
622
+ if (!pool)
623
+ continue;
624
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
625
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
626
+ const price = pool.token?.price ?? 1;
627
+ supplied += supplyBal * price;
628
+ borrowed += borrowBal * price;
629
+ if (supplyBal > 0) {
630
+ weightedLtv += supplyBal * price * parseLtv(pool.ltv);
631
+ weightedLiqThreshold += supplyBal * price * parseLiqThreshold(pool.liquidationFactor.threshold);
632
+ }
633
+ }
634
+ const ltv = supplied > 0 ? weightedLtv / supplied : 0.75;
635
+ const liqThreshold = supplied > 0 ? weightedLiqThreshold / supplied : 0.75;
636
+ const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
637
+ const usdcPool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || '', SUPPORTED_ASSETS.USDC.type));
638
+ let healthFactor;
639
+ if (borrowed <= 0) {
640
+ healthFactor = Infinity;
641
+ }
642
+ else if (usdcPool) {
643
+ try {
644
+ const tx = new Transaction();
645
+ tx.moveCall({
646
+ target: `${config.uiGetter}::calculator_unchecked::dynamic_health_factor`,
647
+ arguments: [
648
+ tx.object(CLOCK),
649
+ tx.object(config.storage),
650
+ tx.object(config.oracle.priceOracle),
651
+ tx.pure.u8(usdcPool.id),
652
+ tx.pure.address(address),
653
+ tx.pure.u8(usdcPool.id),
654
+ tx.pure.u64(0),
655
+ tx.pure.u64(0),
656
+ tx.pure.bool(false),
657
+ ],
658
+ typeArguments: [usdcPool.suiCoinType],
659
+ });
660
+ const result = await client.devInspectTransactionBlock({
661
+ transactionBlock: tx,
662
+ sender: address,
663
+ });
664
+ const decoded = decodeDevInspect(result, bcs.u256());
665
+ if (decoded !== undefined) {
666
+ healthFactor = normalizeHealthFactor(Number(decoded));
667
+ }
668
+ else {
669
+ healthFactor = (supplied * liqThreshold) / borrowed;
670
+ }
671
+ }
672
+ catch {
673
+ healthFactor = (supplied * liqThreshold) / borrowed;
674
+ }
675
+ }
676
+ else {
677
+ healthFactor = (supplied * liqThreshold) / borrowed;
678
+ }
679
+ return {
680
+ healthFactor,
681
+ supplied,
682
+ borrowed,
683
+ maxBorrow: maxBorrowVal,
684
+ liquidationThreshold: liqThreshold,
685
+ };
686
+ }
687
+ const NAVI_SUPPORTED_ASSETS = [...STABLE_ASSETS, 'SUI', 'ETH', 'GOLD'];
688
+ export async function getRates(client) {
689
+ try {
690
+ const pools = await getPools();
691
+ const result = {};
692
+ for (const asset of NAVI_SUPPORTED_ASSETS) {
693
+ const targetType = SUPPORTED_ASSETS[asset].type;
694
+ const pool = pools.find((p) => matchesCoinType(p.suiCoinType || p.coinType || '', targetType));
695
+ if (!pool)
696
+ continue;
697
+ let saveApy = poolSaveApy(pool);
698
+ let borrowApy = poolBorrowApy(pool);
699
+ if (saveApy <= 0 || saveApy > 200)
700
+ saveApy = 0;
701
+ if (borrowApy <= 0 || borrowApy > 200)
702
+ borrowApy = 0;
703
+ result[asset] = { saveApy, borrowApy };
704
+ }
705
+ if (!result.USDC)
706
+ result.USDC = { saveApy: 4.0, borrowApy: 6.0 };
707
+ return result;
708
+ }
709
+ catch {
710
+ return { USDC: { saveApy: 4.0, borrowApy: 6.0 } };
711
+ }
712
+ }
713
+ export async function getPositions(client, addressOrKeypair) {
714
+ const address = typeof addressOrKeypair === 'string'
715
+ ? addressOrKeypair
716
+ : addressOrKeypair.getPublicKey().toSuiAddress();
717
+ const [states, pools] = await Promise.all([getUserState(client, address), getPools()]);
718
+ const positions = [];
719
+ for (const state of states) {
720
+ const pool = pools.find((p) => p.id === state.assetId);
721
+ if (!pool)
722
+ continue;
723
+ const symbol = resolvePoolSymbol(pool);
724
+ const supplyBal = compoundBalance(state.supplyBalance, pool.currentSupplyIndex, pool);
725
+ const borrowBal = compoundBalance(state.borrowBalance, pool.currentBorrowIndex, pool);
726
+ if (supplyBal > 0.0001) {
727
+ positions.push({
728
+ protocol: 'navi',
729
+ asset: symbol,
730
+ type: 'save',
731
+ amount: supplyBal,
732
+ apy: poolSaveApy(pool),
733
+ });
734
+ }
735
+ if (borrowBal > 0.0001) {
736
+ positions.push({
737
+ protocol: 'navi',
738
+ asset: symbol,
739
+ type: 'borrow',
740
+ amount: borrowBal,
741
+ apy: poolBorrowApy(pool),
742
+ });
743
+ }
744
+ }
745
+ return { positions };
746
+ }
747
+ export async function maxWithdrawAmount(client, addressOrKeypair) {
748
+ const hf = await getHealthFactor(client, addressOrKeypair);
749
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
750
+ let maxAmount;
751
+ if (hf.borrowed === 0) {
752
+ maxAmount = hf.supplied;
753
+ }
754
+ else {
755
+ maxAmount = Math.max(0, hf.supplied - (hf.borrowed * MIN_HEALTH_FACTOR / ltv));
756
+ }
757
+ const remainingSupply = hf.supplied - maxAmount;
758
+ const hfAfter = hf.borrowed > 0 ? (remainingSupply * ltv) / hf.borrowed : Infinity;
759
+ return { maxAmount, healthFactorAfter: hfAfter, currentHF: hf.healthFactor };
760
+ }
761
+ export async function maxBorrowAmount(client, addressOrKeypair) {
762
+ const hf = await getHealthFactor(client, addressOrKeypair);
763
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
764
+ const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
765
+ return { maxAmount, healthFactorAfter: MIN_HEALTH_FACTOR, currentHF: hf.healthFactor };
766
+ }
767
+ // ---------------------------------------------------------------------------
768
+ // Claim Rewards
769
+ // ---------------------------------------------------------------------------
770
+ const CERT_TYPE = '0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT';
771
+ const DEEP_TYPE = '0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP';
772
+ const REWARD_FUNDS = {
773
+ [CERT_TYPE]: '0x7093cf7549d5e5b35bfde2177223d1050f71655c7f676a5e610ee70eb4d93b5c',
774
+ [DEEP_TYPE]: '0xc889d78b634f954979e80e622a2ae0fece824c0f6d9590044378a2563035f32f',
775
+ };
776
+ const REWARD_SYMBOLS = {
777
+ [CERT_TYPE]: 'vSUI',
778
+ [DEEP_TYPE]: 'DEEP',
779
+ };
780
+ const REWARD_DECIMALS = {
781
+ [CERT_TYPE]: 9,
782
+ [DEEP_TYPE]: 6,
783
+ };
784
+ let incentiveRulesCache = null;
785
+ async function getIncentiveRules(client) {
786
+ if (incentiveRulesCache && Date.now() - incentiveRulesCache.ts < CACHE_TTL) {
787
+ return incentiveRulesCache.data;
788
+ }
789
+ const [pools, obj] = await Promise.all([
790
+ getPools(),
791
+ client.getObject({
792
+ id: '0x62982dad27fb10bb314b3384d5de8d2ac2d72ab2dbeae5d801dbdb9efa816c80',
793
+ options: { showContent: true },
794
+ }),
795
+ ]);
796
+ const rewardCoinMap = new Map();
797
+ for (const pool of pools) {
798
+ const ct = (pool.suiCoinType || pool.coinType || '').toLowerCase();
799
+ const suffix = ct.split('::').slice(1).join('::');
800
+ const coins = pool.supplyIncentiveApyInfo?.rewardCoin;
801
+ if (Array.isArray(coins) && coins.length > 0) {
802
+ rewardCoinMap.set(suffix, coins[0]);
803
+ }
804
+ }
805
+ const result = new Map();
806
+ if (obj.data?.content?.dataType !== 'moveObject') {
807
+ incentiveRulesCache = { data: result, ts: Date.now() };
808
+ return result;
809
+ }
810
+ const fields = obj.data.content.fields;
811
+ const poolsObj = fields.pools;
812
+ const entries = poolsObj?.fields?.contents;
813
+ if (!Array.isArray(entries)) {
814
+ incentiveRulesCache = { data: result, ts: Date.now() };
815
+ return result;
816
+ }
817
+ for (const entry of entries) {
818
+ const ef = entry?.fields;
819
+ if (!ef)
820
+ continue;
821
+ const key = String(ef.key ?? '');
822
+ const value = ef.value;
823
+ const rules = value?.fields?.rules;
824
+ const ruleEntries = rules?.fields?.contents;
825
+ if (!Array.isArray(ruleEntries))
826
+ continue;
827
+ const ruleIds = ruleEntries.map((re) => {
828
+ const rf = re?.fields;
829
+ return String(rf?.key ?? '');
830
+ }).filter(Boolean);
831
+ const suffix = key.split('::').slice(1).join('::').toLowerCase();
832
+ const full = key.toLowerCase();
833
+ const rewardCoin = rewardCoinMap.get(suffix) ?? rewardCoinMap.get(full) ?? null;
834
+ result.set(key, { ruleIds, rewardCoinType: rewardCoin });
835
+ }
836
+ incentiveRulesCache = { data: result, ts: Date.now() };
837
+ return result;
838
+ }
839
+ function stripPrefix(coinType) {
840
+ return coinType.replace(/^0x0*/, '');
841
+ }
842
+ export async function getPendingRewards(client, address) {
843
+ const [pools, states] = await Promise.all([
844
+ getPools(),
845
+ getUserState(client, address),
846
+ ]);
847
+ const rewards = [];
848
+ const deposited = states.filter((s) => s.supplyBalance > 0n);
849
+ if (deposited.length === 0)
850
+ return rewards;
851
+ for (const state of deposited) {
852
+ const pool = pools.find((p) => p.id === state.assetId);
853
+ if (!pool)
854
+ continue;
855
+ const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? '0');
856
+ if (boostedApr <= 0)
857
+ continue;
858
+ const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
859
+ if (!Array.isArray(rewardCoins) || rewardCoins.length === 0)
860
+ continue;
861
+ const rewardType = rewardCoins[0];
862
+ const assetSymbol = resolvePoolSymbol(pool);
863
+ rewards.push({
864
+ protocol: 'navi',
865
+ asset: assetSymbol,
866
+ coinType: rewardType,
867
+ symbol: REWARD_SYMBOLS[rewardType] ?? rewardType.split('::').pop() ?? 'UNKNOWN',
868
+ amount: 0,
869
+ estimatedValueUsd: 0,
870
+ });
871
+ }
872
+ return rewards;
873
+ }
874
+ export async function addClaimRewardsToTx(tx, client, address) {
875
+ const [config, pools, states, rules] = await Promise.all([
876
+ getConfig(),
877
+ getPools(),
878
+ getUserState(client, address),
879
+ getIncentiveRules(client),
880
+ ]);
881
+ const deposited = states.filter((s) => s.supplyBalance > 0n);
882
+ if (deposited.length === 0)
883
+ return [];
884
+ const claimGroups = new Map();
885
+ for (const state of deposited) {
886
+ const pool = pools.find((p) => p.id === state.assetId);
887
+ if (!pool)
888
+ continue;
889
+ const boostedApr = parseFloat(pool.supplyIncentiveApyInfo?.boostedApr ?? '0');
890
+ if (boostedApr <= 0)
891
+ continue;
892
+ const rewardCoins = pool.supplyIncentiveApyInfo?.rewardCoin;
893
+ if (!Array.isArray(rewardCoins) || rewardCoins.length === 0)
894
+ continue;
895
+ const rewardType = rewardCoins[0];
896
+ const fundId = REWARD_FUNDS[rewardType];
897
+ if (!fundId)
898
+ continue;
899
+ const coinType = pool.suiCoinType || pool.coinType || '';
900
+ const strippedType = stripPrefix(coinType);
901
+ const ruleData = Array.from(rules.entries()).find(([key]) => stripPrefix(key) === strippedType ||
902
+ key.split('::').slice(1).join('::').toLowerCase() ===
903
+ coinType.split('::').slice(1).join('::').toLowerCase());
904
+ if (!ruleData || ruleData[1].ruleIds.length === 0)
905
+ continue;
906
+ const group = claimGroups.get(rewardType) ?? { assets: [], ruleIds: [] };
907
+ for (const ruleId of ruleData[1].ruleIds) {
908
+ group.assets.push(strippedType);
909
+ group.ruleIds.push(ruleId);
910
+ }
911
+ claimGroups.set(rewardType, group);
912
+ }
913
+ const claimed = [];
914
+ for (const [rewardType, { assets, ruleIds }] of claimGroups) {
915
+ const fundId = REWARD_FUNDS[rewardType];
916
+ const [balance] = tx.moveCall({
917
+ target: `${config.package}::incentive_v3::claim_reward`,
918
+ typeArguments: [rewardType],
919
+ arguments: [
920
+ tx.object(CLOCK),
921
+ tx.object(config.incentiveV3),
922
+ tx.object(config.storage),
923
+ tx.object(fundId),
924
+ tx.pure(bcs.vector(bcs.string()).serialize(assets)),
925
+ tx.pure(bcs.vector(bcs.Address).serialize(ruleIds)),
926
+ ],
927
+ });
928
+ const [coin] = tx.moveCall({
929
+ target: '0x2::coin::from_balance',
930
+ typeArguments: [rewardType],
931
+ arguments: [balance],
932
+ });
933
+ tx.transferObjects([coin], address);
934
+ claimed.push({
935
+ protocol: 'navi',
936
+ asset: assets.join(', '),
937
+ coinType: rewardType,
938
+ symbol: REWARD_SYMBOLS[rewardType] ?? 'UNKNOWN',
939
+ amount: 0,
940
+ estimatedValueUsd: 0,
941
+ });
942
+ }
943
+ return claimed;
944
+ }
945
+ //# sourceMappingURL=navi.js.map