@sodax/sdk 2.0.0-rc.12 → 2.0.0-rc.13

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/index.cjs CHANGED
@@ -197,7 +197,8 @@ var SODAX_FEATURES = [
197
197
  "migration",
198
198
  "dex",
199
199
  "partner",
200
- "recovery"
200
+ "recovery",
201
+ "leverageYield"
201
202
  ];
202
203
 
203
204
  // src/shared/utils/tiny-invariant.ts
@@ -559,6 +560,27 @@ var SodaTokens = {
559
560
  vault: "0x243b0c26c8b38793908d7C64e8510f21B19B4613"
560
561
  }
561
562
  };
563
+ var LsodaSymbols = ["lsodaWEETH", "lsodaWSTETH"];
564
+ var LsodaTokens = {
565
+ lsodaWEETH: {
566
+ symbol: "lsodaWEETH",
567
+ name: "Leveraged Soda weETH",
568
+ decimals: 18,
569
+ address: "0xD09de2f5070699A909c0FD32fb5A909d3886701D",
570
+ chainKey: ChainKeys.SONIC_MAINNET,
571
+ hubAsset: "0xD09de2f5070699A909c0FD32fb5A909d3886701D",
572
+ vault: "0xD09de2f5070699A909c0FD32fb5A909d3886701D"
573
+ },
574
+ lsodaWSTETH: {
575
+ symbol: "lsodaWSTETH",
576
+ name: "Leveraged Soda wstETH",
577
+ decimals: 18,
578
+ address: "0x136E5D1CEC5db1829E24941Eddd9C8640E02Ce7a",
579
+ chainKey: ChainKeys.SONIC_MAINNET,
580
+ hubAsset: "0x136E5D1CEC5db1829E24941Eddd9C8640E02Ce7a",
581
+ vault: "0x136E5D1CEC5db1829E24941Eddd9C8640E02Ce7a"
582
+ }
583
+ };
562
584
  var sonicSupportedTokens = {
563
585
  S: {
564
586
  symbol: "S",
@@ -623,7 +645,8 @@ var sonicSupportedTokens = {
623
645
  hubAsset: "0x7c7d53EEcda37a87ce0D5bf8E0b24512A48dC963",
624
646
  vault: SodaTokens.sodaSODA.address
625
647
  },
626
- ...SodaTokens
648
+ ...SodaTokens,
649
+ ...LsodaTokens
627
650
  };
628
651
  var redbellySupportedTokens = {
629
652
  RBNT: {
@@ -3624,7 +3647,8 @@ var swapSupportedTokens = {
3624
3647
  spokeChainConfig[ChainKeys.SONIC_MAINNET].supportedTokens.USDT,
3625
3648
  spokeChainConfig[ChainKeys.SONIC_MAINNET].supportedTokens.wS,
3626
3649
  spokeChainConfig[ChainKeys.SONIC_MAINNET].supportedTokens.SODA,
3627
- ...Object.values(SodaTokens)
3650
+ ...Object.values(SodaTokens),
3651
+ ...Object.values(LsodaTokens)
3628
3652
  ],
3629
3653
  [ChainKeys.AVALANCHE_MAINNET]: [
3630
3654
  spokeChainConfig[ChainKeys.AVALANCHE_MAINNET].supportedTokens.AVAX,
@@ -3864,6 +3888,41 @@ var swapsConfig = {
3864
3888
  var getSupportedSolverTokens = (chainId) => swapSupportedTokens[chainId];
3865
3889
  var isSwapSupportedToken = (chainId, token) => swapSupportedTokens[chainId].some((t) => t.address.toLowerCase() === token.toLowerCase());
3866
3890
 
3891
+ // ../types/dist/leverageYield/leverageYield.js
3892
+ var leverageYieldVaults = [
3893
+ {
3894
+ name: LsodaTokens.lsodaWEETH.symbol,
3895
+ vault: LsodaTokens.lsodaWEETH.vault,
3896
+ asset: SodaTokens.sodaWEETH.address,
3897
+ // sodaWEETH on Sonic
3898
+ borrowToken: SodaTokens.sodaETH.address,
3899
+ // sodaETH on Sonic
3900
+ lsdSource: {
3901
+ // DefiLlama pool for EtherFi's weETH on Ethereum (project: 'ether.fi-stake').
3902
+ poolId: "46bd2bdf-6d92-4066-b482-e885ee172264",
3903
+ fallbackAprPct: 3,
3904
+ label: "EtherFi (weETH)"
3905
+ }
3906
+ },
3907
+ {
3908
+ name: LsodaTokens.lsodaWSTETH.symbol,
3909
+ vault: LsodaTokens.lsodaWSTETH.vault,
3910
+ asset: SodaTokens.sodaWSTETH.address,
3911
+ // sodaWSTETH on Sonic
3912
+ borrowToken: SodaTokens.sodaETH.address,
3913
+ // sodaETH on Sonic
3914
+ lsdSource: {
3915
+ // DefiLlama pool for Lido's stETH on Ethereum (wstETH inherits this rate via redeem).
3916
+ poolId: "747c1d2a-c668-4682-b9f9-296708a3dd90",
3917
+ fallbackAprPct: 2.4,
3918
+ label: "Lido (stETH)"
3919
+ }
3920
+ }
3921
+ ];
3922
+ var leverageYieldConfig = {
3923
+ vaults: leverageYieldVaults
3924
+ };
3925
+
3867
3926
  // ../types/dist/sodax-config/sodax-config.js
3868
3927
  var bridgeConfig = {
3869
3928
  partnerFee: void 0
@@ -3875,6 +3934,7 @@ var sodaxConfig = {
3875
3934
  moneyMarket: moneyMarketConfig,
3876
3935
  bridge: bridgeConfig,
3877
3936
  dex: dexConfig,
3937
+ leverageYield: leverageYieldConfig,
3878
3938
  hub: hubConfig,
3879
3939
  api: apiConfig,
3880
3940
  solver: solverConfig,
@@ -4016,7 +4076,7 @@ function isValidWalletProviderForChainKey(chainKey, walletProvider) {
4016
4076
  }
4017
4077
 
4018
4078
  // ../types/dist/index.js
4019
- var CONFIG_VERSION = 210;
4079
+ var CONFIG_VERSION = 211;
4020
4080
  function isEvmSpokeChainConfig(value) {
4021
4081
  return typeof value === "object" && value !== null && value.chain.type === "EVM" && value.chain.key !== HUB_CHAIN_KEY;
4022
4082
  }
@@ -22295,17 +22355,11 @@ var SuiSpokeService = class {
22295
22355
  ]
22296
22356
  });
22297
22357
  if (params.raw === true) {
22298
- tx.setSender(from);
22299
- const transactionRaw = await tx.build({
22300
- client: this.publicClient,
22301
- onlyTransactionKind: true
22302
- });
22303
- const transactionRawBase64String = Buffer.from(transactionRaw).toString("base64");
22304
22358
  return {
22305
22359
  from,
22306
22360
  to: `${assetManager.packageId}::${assetManager.moduleId}::transfer`,
22307
22361
  value: amount,
22308
- data: transactionRawBase64String
22362
+ data: tx.serialize()
22309
22363
  };
22310
22364
  }
22311
22365
  return params.walletProvider.signAndExecuteTxn(tx);
@@ -22325,17 +22379,11 @@ var SuiSpokeService = class {
22325
22379
  ]
22326
22380
  });
22327
22381
  if (params.raw === true) {
22328
- txb.setSender(from);
22329
- const transactionRaw = await txb.build({
22330
- client: this.publicClient,
22331
- onlyTransactionKind: true
22332
- });
22333
- const transactionRawBase64String = Buffer.from(transactionRaw).toString("base64");
22334
22382
  return {
22335
22383
  from,
22336
22384
  to: `${connection.packageId}::${connection.moduleId}::send_message_ua`,
22337
22385
  value: 0n,
22338
- data: transactionRawBase64String
22386
+ data: txb.serialize()
22339
22387
  };
22340
22388
  }
22341
22389
  return params.walletProvider.signAndExecuteTxn(txb);
@@ -22346,7 +22394,7 @@ var SuiSpokeService = class {
22346
22394
  * @returns {Promise<bigint>} The estimated computation cost.
22347
22395
  */
22348
22396
  async estimateGas({ tx }) {
22349
- const txb = transactions.Transaction.fromKind(tx.data);
22397
+ const txb = transactions.Transaction.from(tx.data);
22350
22398
  const result = await this.publicClient.devInspectTransactionBlock({
22351
22399
  sender: tx.from,
22352
22400
  transactionBlock: txb
@@ -22577,6 +22625,7 @@ var CustomStellarAccount = class {
22577
22625
  decrementSequenceNumber() {
22578
22626
  if (this.sequenceNumber > this.startingSequenceNumber) {
22579
22627
  this.sequenceNumber--;
22628
+ return;
22580
22629
  }
22581
22630
  throw new Error(
22582
22631
  `Sequence number cannot be decremented below the starting sequence number: ${this.startingSequenceNumber}`
@@ -22683,7 +22732,7 @@ var StellarSpokeService = class {
22683
22732
  );
22684
22733
  const assembledPriorityTx = stellarSdk.rpc.assembleTransaction(rawPriorityTx, simulation).build();
22685
22734
  if (params.raw) {
22686
- const transactionXdr = rawPriorityTx.toXDR();
22735
+ const transactionXdr = assembledPriorityTx.toXDR();
22687
22736
  return {
22688
22737
  from,
22689
22738
  to: this.config.getChainConfig(srcChainKey).addresses.assetManager,
@@ -22800,7 +22849,7 @@ var StellarSpokeService = class {
22800
22849
  );
22801
22850
  const assembledPriorityTx = stellarSdk.rpc.assembleTransaction(rawPriorityTx, simulation).build();
22802
22851
  if (params.raw) {
22803
- const transactionXdr = rawPriorityTx.toXDR();
22852
+ const transactionXdr = assembledPriorityTx.toXDR();
22804
22853
  return {
22805
22854
  from,
22806
22855
  to: this.config.getChainConfig(srcChainKey).addresses.assetManager,
@@ -37013,7 +37062,12 @@ var RecoveryService = class {
37013
37062
  srcChainKey: chainKey,
37014
37063
  field: "chainKey"
37015
37064
  });
37016
- const entries = Object.values(chainConfig.supportedTokens).filter((token) => viem.isAddress(token.hubAsset));
37065
+ const leverageVaultAddresses = new Set(
37066
+ this.config.sodaxConfig.leverageYield.vaults.map((vault) => vault.vault.toLowerCase())
37067
+ );
37068
+ const entries = Object.values(chainConfig.supportedTokens).filter(
37069
+ (token) => viem.isAddress(token.hubAsset) && !leverageVaultAddresses.has(token.hubAsset.toLowerCase())
37070
+ );
37017
37071
  if (entries.length === 0) {
37018
37072
  return { ok: true, value: [] };
37019
37073
  }
@@ -37115,6 +37169,871 @@ var RecoveryService = class {
37115
37169
  }
37116
37170
  };
37117
37171
 
37172
+ // src/leverageYield/errors.ts
37173
+ var leverageYieldInvariant = createInvariant("leverageYield");
37174
+ var LEVERAGE_YIELD_CODES = /* @__PURE__ */ new Set([
37175
+ "VALIDATION_FAILED",
37176
+ "INTENT_CREATION_FAILED",
37177
+ "APPROVE_FAILED",
37178
+ "ALLOWANCE_CHECK_FAILED",
37179
+ "LOOKUP_FAILED",
37180
+ "TX_VERIFICATION_FAILED",
37181
+ "TX_SUBMIT_FAILED",
37182
+ "RELAY_TIMEOUT",
37183
+ "RELAY_FAILED",
37184
+ "EXECUTION_FAILED",
37185
+ "EXTERNAL_API_ERROR",
37186
+ "UNKNOWN"
37187
+ ]);
37188
+ var LEVERAGE_YIELD_POST_EXECUTION_CODES = /* @__PURE__ */ new Set([
37189
+ "EXECUTION_FAILED",
37190
+ "EXTERNAL_API_ERROR",
37191
+ "UNKNOWN"
37192
+ ]);
37193
+ var LEVERAGE_YIELD_SWAP_CODES = /* @__PURE__ */ new Set([
37194
+ "VALIDATION_FAILED",
37195
+ "INTENT_CREATION_FAILED",
37196
+ "TX_VERIFICATION_FAILED",
37197
+ "TX_SUBMIT_FAILED",
37198
+ "RELAY_TIMEOUT",
37199
+ "RELAY_FAILED",
37200
+ "EXECUTION_FAILED",
37201
+ "EXTERNAL_API_ERROR",
37202
+ "UNKNOWN"
37203
+ ]);
37204
+ var isLeverageYieldCreateIntentError = isCodeMember(CREATE_INTENT_CODES);
37205
+ var isLeverageYieldApproveError = isCodeMember(APPROVE_CODES);
37206
+ var isLeverageYieldAllowanceCheckError = isCodeMember(ALLOWANCE_CHECK_CODES);
37207
+ var isLeverageYieldLookupError = isCodeMember(LOOKUP_CODES);
37208
+ var isLeverageYieldPostExecutionError = isCodeMember(
37209
+ LEVERAGE_YIELD_POST_EXECUTION_CODES
37210
+ );
37211
+ var isLeverageYieldSwapError = isCodeMember(LEVERAGE_YIELD_SWAP_CODES);
37212
+ var isLeverageYieldError = isCodeMember(LEVERAGE_YIELD_CODES);
37213
+
37214
+ // src/leverageYield/LeverageYieldService.ts
37215
+ var leverageYieldVaultAbi = viem.parseAbi([
37216
+ "function asset() view returns (address)",
37217
+ "function getPositionDetails() view returns (uint256 collateral, uint256 debt, uint256 ltv, uint256 healthFactor, uint256 idleAsset)",
37218
+ // Used by getApr() — read-only metadata that doesn't change per pass.
37219
+ "function pool() view returns (address)",
37220
+ "function borrowToken() view returns (address)",
37221
+ "function targetLTV() view returns (uint256)"
37222
+ ]);
37223
+ var INTENT_DEADLINE_BUFFER_SECONDS = 5 * 60;
37224
+ var ANY_SOLVER_ADDRESS = "0x0000000000000000000000000000000000000000";
37225
+ var MAX_WITHDRAW_DUST_BUFFER = 1000n;
37226
+ function pctToRay(aprPct) {
37227
+ return BigInt(Math.round(aprPct * 1e9)) * 10n ** 16n;
37228
+ }
37229
+ var DEFILLAMA_FETCH_TIMEOUT_MS = 1e4;
37230
+ var DEFILLAMA_FETCH_MAX_ATTEMPTS = 2;
37231
+ async function fetchWithTimeout(url, timeoutMs) {
37232
+ const controller = new AbortController();
37233
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
37234
+ try {
37235
+ return await fetch(url, { signal: controller.signal });
37236
+ } finally {
37237
+ clearTimeout(timeoutId);
37238
+ }
37239
+ }
37240
+ async function fetchDefillamaApr(poolId) {
37241
+ return retry(async () => {
37242
+ const response = await fetchWithTimeout(`https://yields.llama.fi/chart/${poolId}`, DEFILLAMA_FETCH_TIMEOUT_MS);
37243
+ if (!response.ok) throw new Error(`DefiLlama APR fetch failed: HTTP ${response.status}`);
37244
+ const json = await response.json();
37245
+ const series = json?.data;
37246
+ if (!Array.isArray(series) || series.length === 0) {
37247
+ throw new Error("DefiLlama APR response: empty time series");
37248
+ }
37249
+ const latest = series[series.length - 1]?.apy;
37250
+ if (typeof latest !== "number") throw new Error("DefiLlama APR: latest entry missing numeric apy");
37251
+ return latest;
37252
+ }, DEFILLAMA_FETCH_MAX_ATTEMPTS);
37253
+ }
37254
+ var LeverageYieldService = class {
37255
+ hubProvider;
37256
+ config;
37257
+ spoke;
37258
+ constructor({ hubProvider, config, spoke }) {
37259
+ this.hubProvider = hubProvider;
37260
+ this.config = config;
37261
+ this.spoke = spoke;
37262
+ }
37263
+ // ─── Registry ──────────────────────────────────────────────────────────
37264
+ /** Returns the static registry of known leverage-yield vaults. */
37265
+ listVaults() {
37266
+ return this.config.sodaxConfig.leverageYield.vaults;
37267
+ }
37268
+ /** Looks up a vault by its `name` field. Returns `undefined` when not registered. */
37269
+ getVault(name) {
37270
+ return this.listVaults().find((v) => v.name === name);
37271
+ }
37272
+ /**
37273
+ * Looks up a registered vault by its on-chain proxy address (case-insensitive).
37274
+ * Returns `undefined` when the address isn't in the registry.
37275
+ */
37276
+ getVaultByAddress(address) {
37277
+ const normalized = address.toLowerCase();
37278
+ return this.listVaults().find((v) => v.vault.toLowerCase() === normalized);
37279
+ }
37280
+ /**
37281
+ * Resolves the intent `deadline`: returns the caller-supplied value verbatim, otherwise
37282
+ * derives a default from the hub-chain (Sonic) block timestamp plus
37283
+ * {@link INTENT_DEADLINE_BUFFER_SECONDS}. The deadline is enforced on-chain against the hub
37284
+ * block time, so the default is anchored to that block — never the client clock, which can
37285
+ * drift and produce an already-expired or over-extended deadline.
37286
+ *
37287
+ * Returns a {@link Result}: a `getBlock` RPC outage is a read failure, surfaced as
37288
+ * `LOOKUP_FAILED` (`method: 'resolveDeadline'`) rather than masquerading as an intent-build
37289
+ * failure. The `deposit` / `withdraw` callers forward it verbatim.
37290
+ */
37291
+ async resolveDeadline(deadline, srcChainKey) {
37292
+ if (deadline !== void 0) return { ok: true, value: deadline };
37293
+ try {
37294
+ const block = await this.hubProvider.publicClient.getBlock({
37295
+ includeTransactions: false,
37296
+ blockTag: "latest"
37297
+ });
37298
+ return { ok: true, value: block.timestamp + BigInt(INTENT_DEADLINE_BUFFER_SECONDS) };
37299
+ } catch (error) {
37300
+ return { ok: false, error: lookupFailed("leverageYield", "resolveDeadline", error, { srcChainKey }) };
37301
+ }
37302
+ }
37303
+ /**
37304
+ * Builds the {@link LeverageYieldSwapPayload} for a leverage-yield deposit (any token → lsoda*).
37305
+ * The lsoda* output is delivered to the user's hub wallet on Sonic so a later
37306
+ * {@link LeverageYieldService.withdraw} can swap it back. Spread the result into
37307
+ * {@link LeverageYieldService.vaultSwap}: `vaultSwap({ ...payload, walletProvider })`.
37308
+ * An optional `partnerFee` is forwarded on the payload as the per-intent fee override.
37309
+ *
37310
+ * For `minOutputAmount`, quote via `sodax.swaps.getQuote` with the vault address as the
37311
+ * destination token (`token_dst`) — vault shares are solver-tradeable, so the generic swap
37312
+ * quote applies; then subtract your slippage tolerance.
37313
+ */
37314
+ async deposit(params) {
37315
+ const baseCtx = { srcChainKey: params.srcChainKey, action: "deposit" };
37316
+ try {
37317
+ leverageYieldInvariant(params.inputAmount > 0n, "inputAmount must be greater than 0", {
37318
+ ...baseCtx,
37319
+ field: "inputAmount"
37320
+ });
37321
+ leverageYieldInvariant(params.vault.length > 0, "Vault address is required", { ...baseCtx, field: "vault" });
37322
+ leverageYieldInvariant(params.inputToken.length > 0, "inputToken is required", {
37323
+ ...baseCtx,
37324
+ field: "inputToken"
37325
+ });
37326
+ const hubWallet = await this.hubProvider.getUserHubWalletAddress(params.srcAddress, params.srcChainKey);
37327
+ const deadlineResult = await this.resolveDeadline(params.deadline, params.srcChainKey);
37328
+ if (!deadlineResult.ok) return deadlineResult;
37329
+ const deadline = deadlineResult.value;
37330
+ return {
37331
+ ok: true,
37332
+ value: {
37333
+ params: {
37334
+ inputToken: params.inputToken,
37335
+ outputToken: params.vault,
37336
+ inputAmount: params.inputAmount,
37337
+ minOutputAmount: params.minOutputAmount,
37338
+ deadline,
37339
+ allowPartialFill: false,
37340
+ srcChainKey: params.srcChainKey,
37341
+ dstChainKey: this.hubProvider.chainConfig.chain.key,
37342
+ srcAddress: params.srcAddress,
37343
+ dstAddress: hubWallet,
37344
+ solver: params.solver ?? ANY_SOLVER_ADDRESS,
37345
+ data: "0x"
37346
+ },
37347
+ // Per-intent fee override — only included when the caller supplies one, so the
37348
+ // payload stays free of undefined-valued keys.
37349
+ ...params.partnerFee !== void 0 && { partnerFee: params.partnerFee }
37350
+ }
37351
+ };
37352
+ } catch (error) {
37353
+ if (isLeverageYieldCreateIntentError(error)) return { ok: false, error };
37354
+ return { ok: false, error: intentCreationFailed("leverageYield", error, baseCtx) };
37355
+ }
37356
+ }
37357
+ /**
37358
+ * Builds the {@link LeverageYieldSwapPayload} for a leverage-yield withdraw (lsoda* → any
37359
+ * token). The payload carries `hubWalletSwap: true` — {@link LeverageYieldService.vaultSwap}
37360
+ * then spends the lsoda* held in the user's hub wallet by authorising it via a
37361
+ * `Connection.sendMessage` the user signs on `srcChainKey`. Wrapped in a {@link Result} for
37362
+ * a call shape uniform with {@link LeverageYieldService.deposit}; async because the default
37363
+ * `deadline` is read from the hub block timestamp.
37364
+ *
37365
+ * For `minOutputAmount`, quote via `sodax.swaps.getQuote` with the vault address as the
37366
+ * source token (`token_src`) — vault shares are solver-tradeable, so the generic swap quote
37367
+ * applies; then subtract your slippage tolerance.
37368
+ */
37369
+ async withdraw(params) {
37370
+ const baseCtx = { srcChainKey: params.srcChainKey, action: "withdraw" };
37371
+ try {
37372
+ leverageYieldInvariant(params.inputAmount > 0n, "inputAmount must be greater than 0", {
37373
+ ...baseCtx,
37374
+ field: "inputAmount"
37375
+ });
37376
+ leverageYieldInvariant(params.vault.length > 0, "Vault address is required", { ...baseCtx, field: "vault" });
37377
+ leverageYieldInvariant(params.outputToken.length > 0, "outputToken is required", {
37378
+ ...baseCtx,
37379
+ field: "outputToken"
37380
+ });
37381
+ const deadlineResult = await this.resolveDeadline(params.deadline, params.srcChainKey);
37382
+ if (!deadlineResult.ok) return deadlineResult;
37383
+ const deadline = deadlineResult.value;
37384
+ return {
37385
+ ok: true,
37386
+ value: {
37387
+ params: {
37388
+ inputToken: params.vault,
37389
+ outputToken: params.outputToken,
37390
+ inputAmount: params.inputAmount,
37391
+ minOutputAmount: params.minOutputAmount,
37392
+ deadline,
37393
+ allowPartialFill: false,
37394
+ srcChainKey: params.srcChainKey,
37395
+ dstChainKey: params.dstChainKey,
37396
+ srcAddress: params.srcAddress,
37397
+ dstAddress: params.recipient ?? params.srcAddress,
37398
+ solver: params.solver ?? ANY_SOLVER_ADDRESS,
37399
+ data: "0x"
37400
+ },
37401
+ hubWalletSwap: true
37402
+ }
37403
+ };
37404
+ } catch (error) {
37405
+ if (isLeverageYieldCreateIntentError(error)) return { ok: false, error };
37406
+ return { ok: false, error: intentCreationFailed("leverageYield", error, baseCtx) };
37407
+ }
37408
+ }
37409
+ /**
37410
+ * Creates a vault swap intent on the user's source spoke chain without submitting it to
37411
+ * the solver. Leverage-yield copy of the swap domain's `createIntent`, specialised for
37412
+ * vault flows — duplicated deliberately so the vault-specific execution modifiers
37413
+ * (`hubWalletSwap`, per-intent `partnerFee`) stay off the generic swap surface.
37414
+ *
37415
+ * Use {@link LeverageYieldService.vaultSwap} for the full end-to-end flow
37416
+ * (create → relay → notify solver); use this directly when you need the raw transaction
37417
+ * or drive the relay yourself (e.g. the backend submit-tx path). To complete a manual flow,
37418
+ * relay the returned `relayData` (the shared `relayTxAndWaitPacket` helper) and then call
37419
+ * {@link LeverageYieldService.notifySolver} with the hub-side intent tx hash.
37420
+ *
37421
+ * @param _params - Intent parameters, source chain key, wallet provider (when `raw: false`),
37422
+ * and optional `skipSimulation` / `hubWalletSwap` / `partnerFee`.
37423
+ * @returns A `Result<CreateVaultIntentResult<K, Raw>, LeverageYieldCreateIntentError>`.
37424
+ * On success contains:
37425
+ * - `tx` — chain-specific tx hash (executed) or raw tx data (raw mode).
37426
+ * - `intent` — the fully constructed `Intent` object augmented with `feeAmount`.
37427
+ * - `relayData` — `{ address, payload }` needed to submit the intent to the relayer.
37428
+ *
37429
+ * On failure `result.error` is a SodaxError with `VALIDATION_FAILED` (invariant
37430
+ * precondition), `INTENT_CREATION_FAILED` (spoke-side creation/deposit failed) or
37431
+ * `UNKNOWN` (defensive fallback).
37432
+ */
37433
+ async createVaultIntent(_params) {
37434
+ const { params, skipSimulation, hubWalletSwap, partnerFee = this.config.swaps.partnerFee } = _params;
37435
+ const baseCtx = { srcChainKey: params.srcChainKey, dstChainKey: params.dstChainKey };
37436
+ try {
37437
+ leverageYieldInvariant(
37438
+ isUndefinedOrValidWalletProviderForChainKey(params.srcChainKey, _params.walletProvider),
37439
+ `Invalid wallet provider for chain key: ${params.srcChainKey}`,
37440
+ baseCtx
37441
+ );
37442
+ const hubChainKey = this.hubProvider.chainConfig.chain.key;
37443
+ const inputTokenChainKey = hubWalletSwap ? hubChainKey : params.srcChainKey;
37444
+ leverageYieldInvariant(
37445
+ this.config.isValidOriginalAssetAddress(inputTokenChainKey, params.inputToken),
37446
+ `Unsupported spoke chain token (srcChainKey: ${inputTokenChainKey}, inputToken: ${params.inputToken})`,
37447
+ { ...baseCtx, field: "inputToken" }
37448
+ );
37449
+ leverageYieldInvariant(
37450
+ this.config.isValidOriginalAssetAddress(params.dstChainKey, params.outputToken),
37451
+ `Unsupported spoke chain token (params.dstChain: ${params.dstChainKey}, params.outputToken: ${params.outputToken})`,
37452
+ { ...baseCtx, field: "outputToken" }
37453
+ );
37454
+ leverageYieldInvariant(
37455
+ this.config.isValidSpokeChainKey(params.srcChainKey),
37456
+ `Invalid spoke chain (srcChainKey): ${params.srcChainKey}`,
37457
+ { ...baseCtx, field: "srcChainKey" }
37458
+ );
37459
+ leverageYieldInvariant(
37460
+ this.config.isValidSpokeChainKey(params.dstChainKey),
37461
+ `Invalid spoke chain (params.dstChain): ${params.dstChainKey}`,
37462
+ { ...baseCtx, field: "dstChainKey" }
37463
+ );
37464
+ if (isBitcoinChainKey(params.dstChainKey) && params.outputToken === "BTC") {
37465
+ leverageYieldInvariant(
37466
+ params.minOutputAmount >= 546n,
37467
+ `Invalid minOutputAmount (params.minOutputAmount): ${params.minOutputAmount}`,
37468
+ { ...baseCtx, field: "minOutputAmount" }
37469
+ );
37470
+ }
37471
+ const personalAddress = params.srcAddress;
37472
+ let walletAddress = personalAddress;
37473
+ if (isBitcoinChainKeyType(params.srcChainKey) && _params.raw === false) {
37474
+ leverageYieldInvariant(
37475
+ isBitcoinWalletProviderType(_params.walletProvider),
37476
+ `Invalid wallet provider for chain key: ${params.srcChainKey}`,
37477
+ baseCtx
37478
+ );
37479
+ walletAddress = await this.spoke.bitcoin.getEffectiveWalletAddress(personalAddress);
37480
+ await this.spoke.bitcoin.radfi.ensureRadfiAccessToken(_params.walletProvider);
37481
+ }
37482
+ const creatorHubWalletAddress = await this.hubProvider.getUserHubWalletAddress(walletAddress, params.srcChainKey);
37483
+ if (hubWalletSwap) {
37484
+ const [data2, intent2, feeAmount2] = EvmSolverService.constructCreateIntentData(
37485
+ { ...params, srcChainKey: hubChainKey, srcAddress: creatorHubWalletAddress },
37486
+ creatorHubWalletAddress,
37487
+ this.config,
37488
+ partnerFee
37489
+ );
37490
+ const coreSendMessageParams = {
37491
+ srcChainKey: params.srcChainKey,
37492
+ // Personal address — NOT the resolved trading `walletAddress`. SpokeService.sendMessage
37493
+ // re-resolves the effective (Bitcoin trading) address itself; passing the already-resolved
37494
+ // trading address here would double-resolve it (getTradingWallet(tradingAddress) →
37495
+ // "Trading wallet not found"). The trading address is used only for the hub-wallet
37496
+ // derivation above (creatorHubWalletAddress). Mirrors MoneyMarketService borrow/withdraw.
37497
+ srcAddress: personalAddress,
37498
+ dstChainKey: hubChainKey,
37499
+ dstAddress: creatorHubWalletAddress,
37500
+ payload: data2,
37501
+ skipSimulation
37502
+ };
37503
+ const txResult2 = await this.spoke.sendMessage(
37504
+ _params.raw ? { ...coreSendMessageParams, raw: true } : {
37505
+ ...coreSendMessageParams,
37506
+ raw: false,
37507
+ walletProvider: _params.walletProvider
37508
+ }
37509
+ );
37510
+ if (!txResult2.ok) {
37511
+ if (isLeverageYieldCreateIntentError(txResult2.error)) {
37512
+ return { ok: false, error: txResult2.error };
37513
+ }
37514
+ return { ok: false, error: intentCreationFailed("leverageYield", txResult2.error, baseCtx) };
37515
+ }
37516
+ return {
37517
+ ok: true,
37518
+ value: {
37519
+ tx: txResult2.value,
37520
+ intent: { ...intent2, feeAmount: feeAmount2 },
37521
+ relayData: { address: creatorHubWalletAddress, payload: data2 }
37522
+ }
37523
+ };
37524
+ }
37525
+ if (isHubChainKeyType(params.srcChainKey) && isSonicChainKeyType(params.srcChainKey)) {
37526
+ const coreSonicParams = {
37527
+ createIntentParams: params,
37528
+ creatorHubWalletAddress,
37529
+ solverConfig: this.config.solver,
37530
+ fee: partnerFee,
37531
+ hubProvider: this.hubProvider
37532
+ };
37533
+ const [txResult2, intent2, feeAmount2, data2] = await SonicSpokeService.createSwapIntent(
37534
+ _params.raw ? { ...coreSonicParams, raw: true } : {
37535
+ ...coreSonicParams,
37536
+ raw: false,
37537
+ walletProvider: _params.walletProvider
37538
+ }
37539
+ );
37540
+ return {
37541
+ ok: true,
37542
+ value: {
37543
+ tx: txResult2,
37544
+ intent: { ...intent2, feeAmount: feeAmount2 },
37545
+ relayData: { address: intent2.creator, payload: data2 }
37546
+ }
37547
+ };
37548
+ }
37549
+ const [data, intent, feeAmount] = EvmSolverService.constructCreateIntentData(
37550
+ {
37551
+ ...params,
37552
+ srcAddress: walletAddress
37553
+ },
37554
+ creatorHubWalletAddress,
37555
+ this.config,
37556
+ partnerFee
37557
+ );
37558
+ const coreDepositParams = {
37559
+ srcChainKey: params.srcChainKey,
37560
+ srcAddress: walletAddress,
37561
+ to: creatorHubWalletAddress,
37562
+ token: params.inputToken,
37563
+ amount: params.inputAmount,
37564
+ data,
37565
+ skipSimulation
37566
+ };
37567
+ const txResult = await this.spoke.deposit(
37568
+ _params.raw ? {
37569
+ ...coreDepositParams,
37570
+ raw: true
37571
+ } : {
37572
+ ...coreDepositParams,
37573
+ raw: false,
37574
+ walletProvider: _params.walletProvider
37575
+ }
37576
+ );
37577
+ if (!txResult.ok) {
37578
+ if (isLeverageYieldCreateIntentError(txResult.error)) {
37579
+ return { ok: false, error: txResult.error };
37580
+ }
37581
+ return {
37582
+ ok: false,
37583
+ error: intentCreationFailed("leverageYield", txResult.error, baseCtx)
37584
+ };
37585
+ }
37586
+ return {
37587
+ ok: true,
37588
+ value: {
37589
+ tx: txResult.value,
37590
+ intent: { ...intent, feeAmount },
37591
+ relayData: { address: intent.creator, payload: data }
37592
+ }
37593
+ };
37594
+ } catch (error) {
37595
+ if (isLeverageYieldCreateIntentError(error)) return { ok: false, error };
37596
+ return {
37597
+ ok: false,
37598
+ error: intentCreationFailed("leverageYield", error, baseCtx)
37599
+ };
37600
+ }
37601
+ }
37602
+ /**
37603
+ * Executes a full end-to-end leverage-yield vault swap (deposit or withdraw).
37604
+ * Leverage-yield copy of the swap domain's `swap()` orchestrator:
37605
+ * 1. Calls {@link LeverageYieldService.createVaultIntent} to submit the intent
37606
+ * transaction on the source spoke chain.
37607
+ * 2. Verifies the spoke transaction landed on-chain.
37608
+ * 3. For non-hub source chains: submits the spoke tx to the relayer and waits for the
37609
+ * relay packet to land on the hub (Sonic). Skipped when `srcChainKey` is the hub.
37610
+ * 4. Notifies the solver, triggering it to fill the intent.
37611
+ *
37612
+ * Spread a {@link LeverageYieldSwapPayload} from `deposit` / `withdraw` into this method
37613
+ * alongside the wallet provider: `vaultSwap({ ...payload, walletProvider })`.
37614
+ *
37615
+ * @returns A `Result<VaultSwapResponse, LeverageYieldSwapError>`. On success:
37616
+ * - `solverExecutionResponse` — solver acknowledgement (`{ answer: 'OK', intent_hash }`).
37617
+ * - `intent` — the on-chain intent object that was created.
37618
+ * - `intentDeliveryInfo` — source/destination chain keys, tx hashes, and user addresses.
37619
+ *
37620
+ * On failure `result.error` carries one of the create-intent codes plus
37621
+ * `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`,
37622
+ * `EXECUTION_FAILED`, `EXTERNAL_API_ERROR` or `UNKNOWN`.
37623
+ */
37624
+ async vaultSwap(_params) {
37625
+ const { params } = _params;
37626
+ const srcChainKey = params.srcChainKey;
37627
+ const baseCtx = { srcChainKey, dstChainKey: params.dstChainKey, action: "vaultSwap" };
37628
+ try {
37629
+ const timeout = _params.timeout;
37630
+ const createIntentResult = await this.createVaultIntent(_params);
37631
+ if (!createIntentResult.ok) {
37632
+ return { ok: false, error: createIntentResult.error };
37633
+ }
37634
+ const { tx: spokeTxHash, intent, relayData } = createIntentResult.value;
37635
+ const verifyTxHashResult = await this.spoke.verifyTxHash({
37636
+ txHash: spokeTxHash,
37637
+ chainKey: srcChainKey
37638
+ });
37639
+ if (!verifyTxHashResult.ok) {
37640
+ return {
37641
+ ok: false,
37642
+ error: verifyFailed("leverageYield", verifyTxHashResult.error, baseCtx)
37643
+ };
37644
+ }
37645
+ let dstIntentTxHash;
37646
+ if (isHubChainKeyType(srcChainKey)) {
37647
+ dstIntentTxHash = spokeTxHash;
37648
+ } else {
37649
+ const packet = await relayTxAndWaitPacket({
37650
+ srcTxHash: spokeTxHash,
37651
+ data: relayData,
37652
+ chainKey: srcChainKey,
37653
+ relayerApiEndpoint: this.config.relay.relayerApiEndpoint,
37654
+ timeout
37655
+ });
37656
+ if (!packet.ok) {
37657
+ return {
37658
+ ok: false,
37659
+ error: mapRelayFailure(packet.error, { feature: "leverageYield", ...baseCtx })
37660
+ };
37661
+ }
37662
+ dstIntentTxHash = packet.value.dst_tx_hash;
37663
+ }
37664
+ const postExecResult = await this.notifySolver({
37665
+ intent_tx_hash: dstIntentTxHash
37666
+ });
37667
+ if (!postExecResult.ok) {
37668
+ return { ok: false, error: postExecResult.error };
37669
+ }
37670
+ return {
37671
+ ok: true,
37672
+ value: {
37673
+ solverExecutionResponse: postExecResult.value,
37674
+ intent,
37675
+ intentDeliveryInfo: {
37676
+ srcChainKey,
37677
+ srcTxHash: spokeTxHash,
37678
+ srcAddress: params.srcAddress,
37679
+ dstChainKey: params.dstChainKey,
37680
+ dstTxHash: dstIntentTxHash,
37681
+ dstAddress: params.dstAddress
37682
+ }
37683
+ }
37684
+ };
37685
+ } catch (error) {
37686
+ if (isLeverageYieldSwapError(error)) return { ok: false, error };
37687
+ return {
37688
+ ok: false,
37689
+ error: unknownFailed("leverageYield", error, baseCtx)
37690
+ };
37691
+ }
37692
+ }
37693
+ /**
37694
+ * Notifies the solver that the vault intent landed on the hub, triggering it to fill.
37695
+ * Leverage-yield copy of the swap domain's `postExecution` — emits only
37696
+ * `EXECUTION_FAILED` / `EXTERNAL_API_ERROR` / `UNKNOWN` (relay/verify codes appear only
37697
+ * on {@link LeverageYieldService.vaultSwap}, which owns the verify + relay steps).
37698
+ *
37699
+ * Called automatically by {@link LeverageYieldService.vaultSwap} after the relay packet
37700
+ * lands on the hub. Public so callers who created the intent via
37701
+ * {@link LeverageYieldService.createVaultIntent} and relayed it themselves can finish the
37702
+ * flow manually.
37703
+ *
37704
+ * @param request - `{ intent_tx_hash }` — the hub-chain (Sonic) tx hash where the intent
37705
+ * was registered (the relay packet's `dst_tx_hash`, or the spoke tx hash for hub-sourced
37706
+ * intents).
37707
+ */
37708
+ async notifySolver(request) {
37709
+ try {
37710
+ const result = await SolverApiService.postExecution(request, this.config.solver, this.config.logger);
37711
+ if (result.ok) return result;
37712
+ const detail = result.error?.detail ?? {
37713
+ code: -999,
37714
+ // SolverIntentErrorCode.UNKNOWN
37715
+ message: "Solver returned malformed error response"
37716
+ };
37717
+ return {
37718
+ ok: false,
37719
+ error: new SodaxError("EXTERNAL_API_ERROR", detail.message, {
37720
+ feature: "leverageYield",
37721
+ context: {
37722
+ phase: "postExecution",
37723
+ api: "solver",
37724
+ solverCode: detail.code,
37725
+ solverDetail: detail
37726
+ }
37727
+ })
37728
+ };
37729
+ } catch (error) {
37730
+ if (isLeverageYieldPostExecutionError(error)) return { ok: false, error };
37731
+ return { ok: false, error: executionFailed("leverageYield", error, { phase: "postExecution" }) };
37732
+ }
37733
+ }
37734
+ /**
37735
+ * Approves the vault's underlying asset to the leverage vault on Sonic. For callers
37736
+ * interacting with the vault directly on the hub — the swap-style {@link
37737
+ * LeverageYieldService.deposit} flow handles its own approvals.
37738
+ */
37739
+ async approve(params) {
37740
+ const baseCtx = { action: "approve" };
37741
+ try {
37742
+ leverageYieldInvariant(params.amount > 0n, "Amount must be greater than 0", { ...baseCtx, field: "amount" });
37743
+ leverageYieldInvariant(params.vault.length > 0, "Vault address is required", { ...baseCtx, field: "vault" });
37744
+ const assetResult = await this.getAsset(params.vault);
37745
+ if (!assetResult.ok) {
37746
+ return { ok: false, error: approveFailed("leverageYield", assetResult.error, baseCtx) };
37747
+ }
37748
+ const from = await params.walletProvider.getWalletAddress();
37749
+ const baseApprove = {
37750
+ token: assetResult.value,
37751
+ amount: params.amount,
37752
+ from,
37753
+ spender: params.vault
37754
+ };
37755
+ if (params.raw) {
37756
+ const tx2 = await Erc20Service.approve({ ...baseApprove, raw: true });
37757
+ return { ok: true, value: tx2 };
37758
+ }
37759
+ const tx = await Erc20Service.approve({
37760
+ ...baseApprove,
37761
+ raw: false,
37762
+ walletProvider: params.walletProvider
37763
+ });
37764
+ return { ok: true, value: tx };
37765
+ } catch (error) {
37766
+ if (isLeverageYieldApproveError(error)) return { ok: false, error };
37767
+ return { ok: false, error: approveFailed("leverageYield", error, baseCtx) };
37768
+ }
37769
+ }
37770
+ /**
37771
+ * Reads on-chain allowance of the vault's underlying asset for `owner → vault`. Returns
37772
+ * `true` when the allowance covers `amount`. Use before a direct
37773
+ * {@link LeverageYieldService.deposit}.
37774
+ */
37775
+ async isAllowanceValid(params) {
37776
+ const baseCtx = { action: "allowanceCheck" };
37777
+ try {
37778
+ leverageYieldInvariant(params.amount > 0n, "Amount must be greater than 0", { ...baseCtx, field: "amount" });
37779
+ const assetResult = await this.getAsset(params.vault);
37780
+ if (!assetResult.ok) {
37781
+ return { ok: false, error: allowanceCheckFailed("leverageYield", assetResult.error, baseCtx) };
37782
+ }
37783
+ const allowance = await this.hubProvider.publicClient.readContract({
37784
+ address: assetResult.value,
37785
+ abi: viem.erc20Abi,
37786
+ functionName: "allowance",
37787
+ args: [params.owner, params.vault]
37788
+ });
37789
+ return { ok: true, value: allowance >= params.amount };
37790
+ } catch (error) {
37791
+ if (isLeverageYieldAllowanceCheckError(error)) return { ok: false, error };
37792
+ return { ok: false, error: allowanceCheckFailed("leverageYield", error, baseCtx) };
37793
+ }
37794
+ }
37795
+ // ─── Reads ──────────────────────────────────────────────────────────────
37796
+ /** ERC-4626 `asset()` of the vault — the sodaWEETH-style underlying. */
37797
+ async getAsset(vault) {
37798
+ try {
37799
+ const value = await this.hubProvider.publicClient.readContract({
37800
+ address: vault,
37801
+ abi: leverageYieldVaultAbi,
37802
+ functionName: "asset"
37803
+ });
37804
+ return { ok: true, value };
37805
+ } catch (error) {
37806
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
37807
+ return { ok: false, error: lookupFailed("leverageYield", "getAsset", error) };
37808
+ }
37809
+ }
37810
+ /** Reads the vault's leveraged position snapshot via the non-standard `getPositionDetails`. */
37811
+ async getPosition(vault) {
37812
+ try {
37813
+ const [collateral, debt, ltv, healthFactor, idleAsset] = await this.hubProvider.publicClient.readContract({
37814
+ address: vault,
37815
+ abi: leverageYieldVaultAbi,
37816
+ functionName: "getPositionDetails"
37817
+ });
37818
+ return { ok: true, value: { collateral, debt, ltv, healthFactor, idleAsset } };
37819
+ } catch (error) {
37820
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
37821
+ return { ok: false, error: lookupFailed("leverageYield", "getPosition", error) };
37822
+ }
37823
+ }
37824
+ /**
37825
+ * Computes the steady-state APR of a leverage-yield vault from the AAVE supply/borrow
37826
+ * rates of its asset and borrowToken, scaled by the vault's target leverage. Matches the
37827
+ * convention used by leveraged-LSD vaults (Origami, Gearbox, etc.) — assumes constant
37828
+ * LTV at `targetLTV` and constant AAVE rates.
37829
+ *
37830
+ * Returns raw fields in RAY (1e27, AAVE's native rate scale) plus the leverage multiplier
37831
+ * in WAD (1e18). See {@link LeverageYieldApr} for the formula and caveats.
37832
+ */
37833
+ async getApr(vault) {
37834
+ try {
37835
+ const [pool, asset, borrowToken, targetLtvBps] = await Promise.all([
37836
+ this.hubProvider.publicClient.readContract({
37837
+ address: vault,
37838
+ abi: leverageYieldVaultAbi,
37839
+ functionName: "pool"
37840
+ }),
37841
+ this.hubProvider.publicClient.readContract({
37842
+ address: vault,
37843
+ abi: leverageYieldVaultAbi,
37844
+ functionName: "asset"
37845
+ }),
37846
+ this.hubProvider.publicClient.readContract({
37847
+ address: vault,
37848
+ abi: leverageYieldVaultAbi,
37849
+ functionName: "borrowToken"
37850
+ }),
37851
+ this.hubProvider.publicClient.readContract({
37852
+ address: vault,
37853
+ abi: leverageYieldVaultAbi,
37854
+ functionName: "targetLTV"
37855
+ })
37856
+ ]);
37857
+ const [assetReserve, borrowReserve] = await Promise.all([
37858
+ this.hubProvider.publicClient.readContract({
37859
+ address: pool,
37860
+ abi: poolAbi,
37861
+ functionName: "getReserveData",
37862
+ args: [asset]
37863
+ }),
37864
+ this.hubProvider.publicClient.readContract({
37865
+ address: pool,
37866
+ abi: poolAbi,
37867
+ functionName: "getReserveData",
37868
+ args: [borrowToken]
37869
+ })
37870
+ ]);
37871
+ const supplyAprRay = assetReserve.currentLiquidityRate;
37872
+ const borrowAprRay = borrowReserve.currentVariableBorrowRate;
37873
+ const BPS = 10000n;
37874
+ const WAD2 = 1000000000000000000n;
37875
+ leverageYieldInvariant(targetLtvBps < BPS, `targetLTV (${targetLtvBps}) must be < 100% (10_000 bps)`, {
37876
+ method: "getApr",
37877
+ field: "targetLtvBps"
37878
+ });
37879
+ const leverageMultiplierWad = targetLtvBps * WAD2 / (BPS - targetLtvBps);
37880
+ const spreadRay = supplyAprRay - borrowAprRay;
37881
+ const netAprRay = supplyAprRay + spreadRay * leverageMultiplierWad / WAD2;
37882
+ return {
37883
+ ok: true,
37884
+ value: { supplyAprRay, borrowAprRay, targetLtvBps, leverageMultiplierWad, netAprRay }
37885
+ };
37886
+ } catch (error) {
37887
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
37888
+ return { ok: false, error: lookupFailed("leverageYield", "getApr", error) };
37889
+ }
37890
+ }
37891
+ /**
37892
+ * Off-chain LSD staking-APR for the vault's underlying asset. Looks the vault up in the
37893
+ * registry, hits DefiLlama's per-pool chart endpoint for the configured `poolId`, and on
37894
+ * any fetch failure returns the registry's hardcoded `fallbackAprPct` with `stale: true`.
37895
+ *
37896
+ * Always resolves to `{ ok: true, ... }` for a known vault — the fallback path replaces
37897
+ * the error, since a missing LSD APR shouldn't break the parent call. Returns
37898
+ * `{ aprRay: 0n, stale: true, label: 'no LSD source' }` for vaults without an
37899
+ * `lsdSource` configured (non-LSD strategies); callers can treat that as "skip LSD".
37900
+ */
37901
+ async getLsdApr(vault) {
37902
+ try {
37903
+ const cfg = this.getVaultByAddress(vault);
37904
+ const source = cfg?.lsdSource;
37905
+ if (!source) {
37906
+ return { ok: true, value: { aprRay: 0n, label: "no LSD source", stale: true } };
37907
+ }
37908
+ try {
37909
+ const aprPct = await fetchDefillamaApr(source.poolId);
37910
+ return { ok: true, value: { aprRay: pctToRay(aprPct), label: source.label, stale: false } };
37911
+ } catch (fetchError) {
37912
+ this.config.logger.warn("[leverageYield] DefiLlama APR fetch failed; using fallbackAprPct", {
37913
+ vault,
37914
+ poolId: source.poolId,
37915
+ fallbackAprPct: source.fallbackAprPct,
37916
+ error: fetchError instanceof Error ? fetchError.message : String(fetchError)
37917
+ });
37918
+ return {
37919
+ ok: true,
37920
+ value: { aprRay: pctToRay(source.fallbackAprPct), label: `${source.label} (fallback)`, stale: true }
37921
+ };
37922
+ }
37923
+ } catch (error) {
37924
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
37925
+ return { ok: false, error: lookupFailed("leverageYield", "getLsdApr", error) };
37926
+ }
37927
+ }
37928
+ /**
37929
+ * Combined view of {@link getApr} + {@link getLsdApr}: re-applies the vault's leverage
37930
+ * formula with the LSD's native staking yield folded into the supply side, exposing the
37931
+ * **effective** net APR that LSD-backed strategies actually earn. The AAVE-only
37932
+ * `netAprRay` is preserved on the return value for callers who want to display both.
37933
+ *
37934
+ * effectiveSupply = supplyAprRay + lsdApr.aprRay
37935
+ * effectiveNet = effectiveSupply + leverage × (effectiveSupply − borrowAprRay)
37936
+ *
37937
+ * Fetches AAVE rates and the LSD APR in parallel for one round-trip's worth of latency.
37938
+ */
37939
+ async getEffectiveApr(vault) {
37940
+ try {
37941
+ const [aprResult, lsdResult] = await Promise.all([this.getApr(vault), this.getLsdApr(vault)]);
37942
+ if (!aprResult.ok) return aprResult;
37943
+ if (!lsdResult.ok) return lsdResult;
37944
+ const apr = aprResult.value;
37945
+ const lsd = lsdResult.value;
37946
+ const effectiveSupplyAprRay = apr.supplyAprRay + lsd.aprRay;
37947
+ const spreadRay = effectiveSupplyAprRay - apr.borrowAprRay;
37948
+ const WAD2 = 1000000000000000000n;
37949
+ const effectiveNetAprRay = effectiveSupplyAprRay + spreadRay * apr.leverageMultiplierWad / WAD2;
37950
+ return {
37951
+ ok: true,
37952
+ value: { ...apr, lsdApr: lsd, effectiveSupplyAprRay, effectiveNetAprRay }
37953
+ };
37954
+ } catch (error) {
37955
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
37956
+ return { ok: false, error: lookupFailed("leverageYield", "getEffectiveApr", error) };
37957
+ }
37958
+ }
37959
+ /** Synchronously withdrawable assets for `owner` (clamped against leverage HF floor). */
37960
+ async getMaxWithdraw(vault, owner) {
37961
+ const inner = await Erc4626Service.getMaxWithdraw(vault, owner, this.hubProvider.publicClient);
37962
+ if (!inner.ok) return { ok: false, error: lookupFailed("leverageYield", "getMaxWithdraw", inner.error) };
37963
+ return { ok: true, value: inner.value };
37964
+ }
37965
+ /** Total underlying assets currently held by the vault (vault-asset units, 18 decimals) — TVL. */
37966
+ async getTotalAssets(vault) {
37967
+ const inner = await Erc4626Service.getTotalAssets(vault, this.hubProvider.publicClient);
37968
+ if (!inner.ok) return { ok: false, error: lookupFailed("leverageYield", "getTotalAssets", inner.error) };
37969
+ return { ok: true, value: inner.value };
37970
+ }
37971
+ /** Shares minted for a given asset deposit. */
37972
+ async previewDeposit(vault, assets) {
37973
+ const inner = await Erc4626Service.previewDeposit(vault, assets, this.hubProvider.publicClient);
37974
+ if (!inner.ok) return { ok: false, error: lookupFailed("leverageYield", "previewDeposit", inner.error) };
37975
+ return { ok: true, value: inner.value };
37976
+ }
37977
+ /** Shares burned for a given asset withdrawal. */
37978
+ async previewWithdraw(vault, assets) {
37979
+ const inner = await Erc4626Service.previewWithdraw(vault, assets, this.hubProvider.publicClient);
37980
+ if (!inner.ok) return { ok: false, error: lookupFailed("leverageYield", "previewWithdraw", inner.error) };
37981
+ return { ok: true, value: inner.value };
37982
+ }
37983
+ /** Assets received for a given share redemption. */
37984
+ async previewRedeem(vault, shares) {
37985
+ const inner = await Erc4626Service.previewRedeem(vault, shares, this.hubProvider.publicClient);
37986
+ if (!inner.ok) return { ok: false, error: lookupFailed("leverageYield", "previewRedeem", inner.error) };
37987
+ return { ok: true, value: inner.value };
37988
+ }
37989
+ /** Vault shares held by `owner`. */
37990
+ async getShareBalance(vault, owner) {
37991
+ try {
37992
+ const value = await this.hubProvider.publicClient.readContract({
37993
+ address: vault,
37994
+ abi: viem.erc20Abi,
37995
+ functionName: "balanceOf",
37996
+ args: [owner]
37997
+ });
37998
+ return { ok: true, value };
37999
+ } catch (error) {
38000
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
38001
+ return { ok: false, error: lookupFailed("leverageYield", "getShareBalance", error) };
38002
+ }
38003
+ }
38004
+ /**
38005
+ * Convenience: resolves the user's hub wallet from `(srcChainKey, srcAddress)` and
38006
+ * returns its on-chain `maxWithdraw`, less {@link MAX_WITHDRAW_DUST_BUFFER}. The trim
38007
+ * keeps the value safe for an asset-denominated ERC-4626 `withdraw` — the raw
38008
+ * `maxWithdraw` can trip the round-up that asks for one more share than the user holds.
38009
+ */
38010
+ async getMaxWithdrawForUser(vault, srcChainKey, srcAddress) {
38011
+ try {
38012
+ const hubWallet = await this.hubProvider.getUserHubWalletAddress(srcAddress, srcChainKey);
38013
+ const maxWithdrawResult = await this.getMaxWithdraw(vault, hubWallet);
38014
+ if (!maxWithdrawResult.ok) return maxWithdrawResult;
38015
+ const buffered = maxWithdrawResult.value > MAX_WITHDRAW_DUST_BUFFER ? maxWithdrawResult.value - MAX_WITHDRAW_DUST_BUFFER : 0n;
38016
+ return { ok: true, value: buffered };
38017
+ } catch (error) {
38018
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
38019
+ return { ok: false, error: lookupFailed("leverageYield", "getMaxWithdrawForUser", error, { srcChainKey }) };
38020
+ }
38021
+ }
38022
+ /**
38023
+ * Convenience: resolves the user's hub wallet from `(srcChainKey, srcAddress)` and
38024
+ * returns its on-chain share balance.
38025
+ */
38026
+ async getShareBalanceForUser(vault, srcChainKey, srcAddress) {
38027
+ try {
38028
+ const hubWallet = await this.hubProvider.getUserHubWalletAddress(srcAddress, srcChainKey);
38029
+ return await this.getShareBalance(vault, hubWallet);
38030
+ } catch (error) {
38031
+ if (isLeverageYieldLookupError(error)) return { ok: false, error };
38032
+ return { ok: false, error: lookupFailed("leverageYield", "getShareBalanceForUser", error, { srcChainKey }) };
38033
+ }
38034
+ }
38035
+ };
38036
+
37118
38037
  // src/shared/entities/Sodax.ts
37119
38038
  var Sodax = class {
37120
38039
  instanceConfig;
@@ -37136,6 +38055,8 @@ var Sodax = class {
37136
38055
  // Recovery service for withdrawing stuck hub-wallet assets back to a spoke chain
37137
38056
  dex;
37138
38057
  // Dex service enabling DEX operations
38058
+ leverageYield;
38059
+ // Leverage-yield service: cross-chain deposits / withdrawals into ERC-4626 leverage vaults on Sonic
37139
38060
  config;
37140
38061
  // Config service enabling configuration data fetching from the backend API or fallbacking to default values
37141
38062
  hubProvider;
@@ -37181,6 +38102,11 @@ var Sodax = class {
37181
38102
  config: this.config,
37182
38103
  spoke: this.spoke
37183
38104
  });
38105
+ this.leverageYield = new LeverageYieldService({
38106
+ hubProvider: this.hubProvider,
38107
+ config: this.config,
38108
+ spoke: this.spoke
38109
+ });
37184
38110
  }
37185
38111
  /**
37186
38112
  * Initializes the Sodax instance with dynamic configuration.
@@ -37289,8 +38215,11 @@ exports.IntentsAbi = IntentsAbi;
37289
38215
  exports.LOOKUP_CODES = LOOKUP_CODES;
37290
38216
  exports.LTV_PRECISION = LTV_PRECISION;
37291
38217
  exports.LendingPoolService = LendingPoolService;
38218
+ exports.LeverageYieldService = LeverageYieldService;
37292
38219
  exports.LockupMultiplier = LockupMultiplier;
37293
38220
  exports.LockupPeriod = LockupPeriod;
38221
+ exports.LsodaSymbols = LsodaSymbols;
38222
+ exports.LsodaTokens = LsodaTokens;
37294
38223
  exports.MAX_UINT256 = MAX_UINT256;
37295
38224
  exports.MigrationService = MigrationService;
37296
38225
  exports.MintPositionEventAbi = MintPositionEventAbi;
@@ -37475,6 +38404,13 @@ exports.isInjectiveChainKeyType = isInjectiveChainKeyType;
37475
38404
  exports.isJsonRpcPayloadResponse = isJsonRpcPayloadResponse;
37476
38405
  exports.isLegacybnUSDChainId = isLegacybnUSDChainId;
37477
38406
  exports.isLegacybnUSDToken = isLegacybnUSDToken;
38407
+ exports.isLeverageYieldAllowanceCheckError = isLeverageYieldAllowanceCheckError;
38408
+ exports.isLeverageYieldApproveError = isLeverageYieldApproveError;
38409
+ exports.isLeverageYieldCreateIntentError = isLeverageYieldCreateIntentError;
38410
+ exports.isLeverageYieldError = isLeverageYieldError;
38411
+ exports.isLeverageYieldLookupError = isLeverageYieldLookupError;
38412
+ exports.isLeverageYieldPostExecutionError = isLeverageYieldPostExecutionError;
38413
+ exports.isLeverageYieldSwapError = isLeverageYieldSwapError;
37478
38414
  exports.isMigrateOrchestrationError = isMigrateOrchestrationError;
37479
38415
  exports.isMigrationAllowanceCheckError = isMigrationAllowanceCheckError;
37480
38416
  exports.isMigrationApproveError = isMigrationApproveError;
@@ -37541,6 +38477,9 @@ exports.isUndefinedOrValidWalletProviderForChainKey = isUndefinedOrValidWalletPr
37541
38477
  exports.isUnifiedBnUSDMigrateParams = isUnifiedBnUSDMigrateParams;
37542
38478
  exports.isValidWalletProviderForChainKey = isValidWalletProviderForChainKey;
37543
38479
  exports.kaiaSupportedTokens = kaiaSupportedTokens;
38480
+ exports.leverageYieldConfig = leverageYieldConfig;
38481
+ exports.leverageYieldInvariant = leverageYieldInvariant;
38482
+ exports.leverageYieldVaults = leverageYieldVaults;
37544
38483
  exports.lightlinkSupportedTokens = lightlinkSupportedTokens;
37545
38484
  exports.lookupFailed = lookupFailed;
37546
38485
  exports.mapRelayFailure = mapRelayFailure;