@haven-fi/solauto-sdk 1.0.835 → 1.0.837

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 (45) hide show
  1. package/dist/constants/switchboardConstants.d.ts.map +1 -1
  2. package/dist/constants/switchboardConstants.js +2 -25
  3. package/dist/externalSdks/marginfi/types/oracleSetup.d.ts +22 -1
  4. package/dist/externalSdks/marginfi/types/oracleSetup.d.ts.map +1 -1
  5. package/dist/externalSdks/marginfi/types/oracleSetup.js +21 -0
  6. package/dist/services/flashLoans/marginfiFlProvider.d.ts.map +1 -1
  7. package/dist/services/flashLoans/marginfiFlProvider.js +5 -8
  8. package/dist/services/solauto/solautoMarginfiClient.d.ts +3 -0
  9. package/dist/services/solauto/solautoMarginfiClient.d.ts.map +1 -1
  10. package/dist/services/solauto/solautoMarginfiClient.js +22 -12
  11. package/dist/services/transactions/manager/transactionsManager.d.ts.map +1 -1
  12. package/dist/services/transactions/manager/transactionsManager.js +2 -1
  13. package/dist/solautoPosition/marginfiSolautoPositionEx.d.ts.map +1 -1
  14. package/dist/solautoPosition/marginfiSolautoPositionEx.js +4 -2
  15. package/dist/utils/marginfi/data.d.ts.map +1 -1
  16. package/dist/utils/marginfi/data.js +9 -15
  17. package/dist/utils/marginfi/general.d.ts +5 -0
  18. package/dist/utils/marginfi/general.d.ts.map +1 -1
  19. package/dist/utils/marginfi/general.js +36 -8
  20. package/dist/utils/marginfi/index.d.ts +1 -0
  21. package/dist/utils/marginfi/index.d.ts.map +1 -1
  22. package/dist/utils/marginfi/index.js +1 -0
  23. package/dist/utils/marginfi/prices.d.ts +14 -0
  24. package/dist/utils/marginfi/prices.d.ts.map +1 -0
  25. package/dist/utils/marginfi/prices.js +119 -0
  26. package/dist/utils/switchboardUtils.d.ts +5 -3
  27. package/dist/utils/switchboardUtils.d.ts.map +1 -1
  28. package/dist/utils/switchboardUtils.js +35 -12
  29. package/local/txSandbox.ts +3 -3
  30. package/package.json +1 -1
  31. package/src/constants/switchboardConstants.ts +2 -27
  32. package/src/externalSdks/marginfi/types/oracleSetup.ts +21 -0
  33. package/src/services/flashLoans/marginfiFlProvider.ts +10 -11
  34. package/src/services/solauto/solautoMarginfiClient.ts +34 -12
  35. package/src/services/transactions/manager/transactionsManager.ts +4 -1
  36. package/src/solautoPosition/marginfiSolautoPositionEx.ts +14 -3
  37. package/src/utils/marginfi/data.ts +16 -16
  38. package/src/utils/marginfi/general.ts +46 -12
  39. package/src/utils/marginfi/index.ts +2 -1
  40. package/src/utils/marginfi/prices.ts +165 -0
  41. package/src/utils/switchboardUtils.ts +55 -17
  42. package/tests/fixtures/README.md +17 -0
  43. package/tests/fixtures/marginfiOracles.json +82 -0
  44. package/tests/unit/marginfiOracles.ts +333 -0
  45. package/tests/unit/transactionsManager.ts +44 -1
@@ -0,0 +1,333 @@
1
+ import { assert } from "chai";
2
+ import {
3
+ publicKey,
4
+ RpcAccount,
5
+ Umi,
6
+ createNoopSigner,
7
+ } from "@metaplex-foundation/umi";
8
+ import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
9
+ import { PublicKey } from "@solana/web3.js";
10
+ import {
11
+ composeRemainingAccounts,
12
+ getMarginfiBankHealthAccounts,
13
+ getMarginfiBankPrice,
14
+ getPythLstPriceData,
15
+ safeGetMarginfiBankPrice,
16
+ addSwbOraclePullTxs,
17
+ getMarginfiSwitchboardFeeds,
18
+ LendingPlatform,
19
+ PriceType,
20
+ PRICES,
21
+ SWITCHBOARD_PRICE_FEED_IDS,
22
+ SolautoMarginfiClient,
23
+ getMarginfiAccounts,
24
+ getAccountMeta,
25
+ RebalanceStep,
26
+ RebalanceDirection,
27
+ SolautoRebalanceType,
28
+ } from "../../src";
29
+ import {
30
+ deserializeBank,
31
+ OracleSetup,
32
+ PriceBias,
33
+ } from "../../src/externalSdks/marginfi";
34
+ import fixtures from "../fixtures/marginfiOracles.json";
35
+
36
+ function account(value: {
37
+ publicKey: string;
38
+ owner: string;
39
+ data: string;
40
+ }): RpcAccount {
41
+ return {
42
+ ...value,
43
+ publicKey: publicKey(value.publicKey),
44
+ owner: publicKey(value.owner),
45
+ data: Buffer.from(value.data, "base64"),
46
+ exists: true,
47
+ } as RpcAccount;
48
+ }
49
+
50
+ const bank = (ticker: string) =>
51
+ deserializeBank(account(fixtures.banks.find((b) => b.ticker === ticker)!));
52
+ const oracle = (address: string) =>
53
+ account(fixtures.oracles.find((o) => o.publicKey === address)!);
54
+ const clock = () => account(fixtures.oracles[4]);
55
+ const noRpc = {} as Umi;
56
+
57
+ describe("Marginfi oracle migration", () => {
58
+ it("decodes the migrated mainnet banks without changing discriminator values", () => {
59
+ for (const ticker of ["LST", "hSOL", "JupSOL"]) {
60
+ assert.equal(bank(ticker).config.oracleSetup, OracleSetup.PythLST);
61
+ }
62
+ for (const ticker of ["SOL", "JitoSOL", "JTO"]) {
63
+ assert.equal(bank(ticker).config.oracleSetup, OracleSetup.PythPushOracle);
64
+ }
65
+ assert.equal(OracleSetup.PythLST, 22);
66
+ assert.equal(bank("USDC").config.oracleSetup, OracleSetup.Fixed);
67
+ });
68
+
69
+ it("does not crank the six banks that migrated away from Switchboard", async () => {
70
+ const banks = fixtures.banks
71
+ .filter((b) => b.env === "Prod")
72
+ .map((b) => bank(b.ticker));
73
+ assert.deepEqual(getMarginfiSwitchboardFeeds(banks), {});
74
+ for (const b of banks)
75
+ assert.isUndefined(SWITCHBOARD_PRICE_FEED_IDS[b.mint]);
76
+ let calls = 0;
77
+ const client: any = {
78
+ lendingPlatform: LendingPlatform.Marginfi,
79
+ pos: {
80
+ supplyMint: new PublicKey(banks[0].mint),
81
+ debtMint: new PublicKey(banks[3].mint),
82
+ lpSupplyAccount: new PublicKey(banks[0].publicKey),
83
+ lpDebtAccount: new PublicKey(banks[3].publicKey),
84
+ },
85
+ umi: {
86
+ rpc: {
87
+ getAccounts: async (keys: string[]) => {
88
+ calls++;
89
+ return keys.map((key) =>
90
+ account(fixtures.banks.find((b) => b.publicKey === key)!)
91
+ );
92
+ },
93
+ },
94
+ },
95
+ connection: {
96
+ getSlot: () => {
97
+ throw new Error("Should not read a Switchboard feed");
98
+ },
99
+ },
100
+ };
101
+ const txs: any[] = [{ oracleInteractor: true }];
102
+ await addSwbOraclePullTxs(client, txs);
103
+ assert.equal(calls, 1);
104
+ assert.lengthOf(txs, 1);
105
+ });
106
+
107
+ it("uses the bank's configured Switchboard address even for an unmapped mint", () => {
108
+ const b = bank("SOL");
109
+ b.config.oracleSetup = OracleSetup.SwitchboardPull;
110
+ b.config.oracleKeys[0] = publicKey(
111
+ "4Hmd6PdjVA9auCoScE12iaBogfwS4ZXQ6VZoBeqanwWW"
112
+ );
113
+ assert.deepEqual(getMarginfiSwitchboardFeeds([b]), {
114
+ [b.mint]: b.config.oracleKeys[0],
115
+ });
116
+ });
117
+
118
+ it("keeps stake pools with their banks when sorting mixed health-account groups", async () => {
119
+ const lst = bank("LST");
120
+ const sol = bank("SOL");
121
+ const lstGroup = await getMarginfiBankHealthAccounts(noRpc, lst);
122
+ const solGroup = await getMarginfiBankHealthAccounts(
123
+ noRpc,
124
+ sol,
125
+ new PublicKey(sol.config.oracleKeys[0])
126
+ );
127
+ assert.deepEqual(
128
+ lstGroup.map((a) => a.pubkey),
129
+ [lst.publicKey, ...lst.config.oracleKeys.slice(0, 2)]
130
+ );
131
+ assert.deepEqual(
132
+ composeRemainingAccounts([...solGroup, ...lstGroup]).map((a) => a.pubkey),
133
+ [...lstGroup, ...solGroup].map((a) => a.pubkey)
134
+ );
135
+ // Repeated base oracles must appear in each bank group, including two LSTs.
136
+ const hsol = await getMarginfiBankHealthAccounts(noRpc, bank("hSOL"));
137
+ const mixed = composeRemainingAccounts([...lstGroup, ...hsol, ...solGroup]);
138
+ assert.lengthOf(mixed, 8);
139
+ assert.equal(
140
+ mixed.filter((a) => a.pubkey === lst.config.oracleKeys[0]).length,
141
+ 3
142
+ );
143
+ });
144
+
145
+ it("prices SPL and both Sanctum stake-pool variants from the Pyth base feed", () => {
146
+ const expected = {
147
+ LST: 152.58419233,
148
+ hSOL: 117.37554225,
149
+ JupSOL: 119.63081594,
150
+ };
151
+ for (const [ticker, price] of Object.entries(expected)) {
152
+ const b = bank(ticker);
153
+ const prices = getPythLstPriceData(
154
+ b,
155
+ oracle(b.config.oracleKeys[0]),
156
+ oracle(b.config.oracleKeys[1]),
157
+ clock()
158
+ );
159
+ assert.closeTo(prices.realtimePrice, price, 1e-8);
160
+ assert.isAbove(prices.emaPrice, prices.realtimePrice);
161
+ }
162
+ });
163
+
164
+ it("includes the stake pool in Solauto refresh, interaction, rebalance and direct borrow instructions", async () => {
165
+ const supply = bank("LST");
166
+ const debt = bank("SOL");
167
+ const key = new PublicKey(supply.publicKey);
168
+ const pool = getAccountMeta(new PublicKey(supply.config.oracleKeys[1]));
169
+ const client: any = Object.assign(
170
+ Object.create(SolautoMarginfiClient.prototype),
171
+ {
172
+ umi: createUmi("https://example.invalid"),
173
+ signer: createNoopSigner(publicKey(key)),
174
+ mfiAccounts: getMarginfiAccounts("Prod"),
175
+ marginfiGroup: new PublicKey(supply.group),
176
+ marginfiAccount: key,
177
+ marginfiAccountPk: key,
178
+ marginfiSupplyAccounts:
179
+ getMarginfiAccounts("Prod").bankAccounts[supply.group][supply.mint],
180
+ marginfiDebtAccounts:
181
+ getMarginfiAccounts("Prod").bankAccounts[debt.group][debt.mint],
182
+ supplyPriceOracle: new PublicKey(supply.config.oracleKeys[0]),
183
+ debtPriceOracle: new PublicKey(debt.config.oracleKeys[0]),
184
+ auxiliaryOracleAccounts: [pool],
185
+ pos: {
186
+ publicKey: key,
187
+ selfManaged: false,
188
+ supplyMint: new PublicKey(supply.mint),
189
+ debtMint: new PublicKey(debt.mint),
190
+ },
191
+ positionSupplyTa: key,
192
+ positionDebtTa: key,
193
+ signerSupplyTa: key,
194
+ signerDebtTa: key,
195
+ solautoFeesSupplyTa: key,
196
+ solautoFeesDebtTa: key,
197
+ referralState: key,
198
+ authority: key,
199
+ healthCheckRemainingAccounts: await getMarginfiBankHealthAccounts(
200
+ noRpc,
201
+ supply
202
+ ),
203
+ debtBankHealthAccounts: await getMarginfiBankHealthAccounts(
204
+ noRpc,
205
+ debt,
206
+ new PublicKey(debt.config.oracleKeys[0])
207
+ ),
208
+ }
209
+ );
210
+ const instructions = [
211
+ client.refreshIx(),
212
+ client.marginfiSolautoProtocolInteractionIx({
213
+ __kind: "Borrow",
214
+ fields: [1n],
215
+ }),
216
+ client.rebalanceIx(RebalanceStep.PreSwap, {
217
+ rebalanceType: SolautoRebalanceType.Regular,
218
+ priceType: PriceType.Realtime,
219
+ values: { rebalanceDirection: RebalanceDirection.Boost },
220
+ swapQuote: {
221
+ inputMint: supply.mint,
222
+ inAmount: "1",
223
+ swapMode: "ExactIn",
224
+ },
225
+ }),
226
+ ];
227
+ for (const tx of instructions) {
228
+ const keys = tx.getInstructions()[0].keys;
229
+ assert.equal(keys[keys.length - 1].pubkey, pool.pubkey);
230
+ assert.isFalse(keys[keys.length - 1].isWritable);
231
+ }
232
+ client.pos.selfManaged = true;
233
+ const borrow = client
234
+ .marginfiProtocolInteractionIx({ __kind: "Borrow", fields: [1n] })
235
+ .getInstructions()[0];
236
+ assert.deepEqual(
237
+ borrow.keys.slice(-5).map((a: any) => a.pubkey),
238
+ [
239
+ supply.publicKey,
240
+ supply.config.oracleKeys[0],
241
+ pool.pubkey,
242
+ debt.publicKey,
243
+ debt.config.oracleKeys[0],
244
+ ]
245
+ );
246
+ });
247
+
248
+ it("preserves exchange-rate-adjusted EMA and confidence independently of generic prices", async () => {
249
+ const b = bank("LST");
250
+ const umi = {
251
+ rpc: { getAccounts: async (keys: string[]) => keys.map(oracle) },
252
+ } as unknown as Umi;
253
+ assert.closeTo(
254
+ await getMarginfiBankPrice(umi, b, PriceType.Ema, PriceBias.Low),
255
+ 152.8792992004,
256
+ 1e-8
257
+ );
258
+ PRICES[b.mint] = {
259
+ realtimePrice: 1,
260
+ emaPrice: 1,
261
+ confInterval: 0,
262
+ emaConfInterval: 0,
263
+ time: 0,
264
+ };
265
+ assert.closeTo(
266
+ safeGetMarginfiBankPrice(b, PriceType.Realtime, PriceBias.High)!,
267
+ 152.6201380112,
268
+ 1e-8
269
+ );
270
+ delete PRICES[b.mint];
271
+ });
272
+
273
+ it("reads the staging fixed price from the bank without fetching an oracle", async () => {
274
+ assert.closeTo(await getMarginfiBankPrice(noRpc, bank("USDC")), 1, 1e-8);
275
+ });
276
+
277
+ it("rejects incorrect, malformed, stale, or unverified pricing accounts", () => {
278
+ const b = bank("LST");
279
+ const mutations: Array<
280
+ (o: RpcAccount, p: RpcAccount, c: RpcAccount) => void
281
+ > = [
282
+ (_, p) => {
283
+ p.owner = publicKey(PublicKey.default);
284
+ },
285
+ (_, p) => {
286
+ p.publicKey = publicKey(PublicKey.default);
287
+ },
288
+ (_, p) => {
289
+ p.data = p.data.slice(0, 281);
290
+ },
291
+ (_, p) => {
292
+ p.data[0] = 0;
293
+ },
294
+ (_, p) => {
295
+ p.data[162] ^= 1;
296
+ },
297
+ (_, p) => {
298
+ (p.data as Buffer).writeBigUInt64LE(0n, 266);
299
+ },
300
+ (_, p) => {
301
+ (p.data as Buffer).writeBigUInt64LE(1n, 266);
302
+ },
303
+ (_, p) => {
304
+ (p.data as Buffer).writeBigUInt64LE(0n, 274);
305
+ },
306
+ (o) => {
307
+ o.owner = publicKey(PublicKey.default);
308
+ },
309
+ (o) => {
310
+ o.publicKey = publicKey(PublicKey.default);
311
+ },
312
+ (o) => {
313
+ o.data[0] ^= 1;
314
+ },
315
+ (o) => {
316
+ o.data[40] = 0;
317
+ },
318
+ (o) => {
319
+ (o.data as Buffer).writeBigInt64LE(0n, 93);
320
+ },
321
+ (_, __, c) => {
322
+ c.publicKey = publicKey(PublicKey.default);
323
+ },
324
+ ];
325
+ for (const mutate of mutations) {
326
+ const o = oracle(b.config.oracleKeys[0]);
327
+ const p = oracle(b.config.oracleKeys[1]);
328
+ const c = clock();
329
+ mutate(o, p, c);
330
+ assert.throws(() => getPythLstPriceData(b, o, p, c));
331
+ }
332
+ });
333
+ });
@@ -1,6 +1,6 @@
1
1
  import { expect } from "chai";
2
2
  import { describe, it } from "mocha";
3
- import { TransactionsManager } from "../../src";
3
+ import { TransactionsManager, UPDATE_ORACLE_TX_NAME } from "../../src";
4
4
 
5
5
  function createManager(abortController?: AbortController) {
6
6
  return new TransactionsManager({
@@ -14,6 +14,49 @@ function createManager(abortController?: AbortController) {
14
14
  }
15
15
 
16
16
  describe("TransactionsManager bundle fallback", () => {
17
+ it("sends a standalone oracle update when later transactions are pending", async () => {
18
+ const manager = createManager();
19
+ const sent: string[] = [];
20
+ const oracleTx = { getInstructions: () => [{}] };
21
+ const oracleSet = {
22
+ items: [{ name: UPDATE_ORACLE_TX_NAME }],
23
+ name: () => UPDATE_ORACLE_TX_NAME,
24
+ getSingleTransaction: async () => oracleTx,
25
+ };
26
+ const rebalanceSet = {
27
+ items: [{ name: "rebalance" }],
28
+ name: () => "rebalance",
29
+ };
30
+
31
+ manager.debugAccounts = async () => {};
32
+ manager.sendTransaction = async (_tx: unknown, name: string) => {
33
+ sent.push(name);
34
+ };
35
+
36
+ await manager.processTransactionSet([oracleSet, rebalanceSet], 0);
37
+
38
+ expect(sent).to.deep.equal([UPDATE_ORACLE_TX_NAME]);
39
+ });
40
+
41
+ it("still skips an oracle update when it is the only remaining transaction", async () => {
42
+ const manager = createManager();
43
+ let sends = 0;
44
+ const oracleSet = {
45
+ items: [{ name: UPDATE_ORACLE_TX_NAME }],
46
+ name: () => UPDATE_ORACLE_TX_NAME,
47
+ getSingleTransaction: async () => ({ getInstructions: () => [{}] }),
48
+ };
49
+
50
+ manager.debugAccounts = async () => {};
51
+ manager.sendTransaction = async () => {
52
+ sends++;
53
+ };
54
+
55
+ await manager.processTransactionSet([oracleSet], 0);
56
+
57
+ expect(sends).to.equal(0);
58
+ });
59
+
17
60
  it("retries a failed multi-transaction bundle sequentially", async () => {
18
61
  const manager = createManager();
19
62
  const calls: string[] = [];