@sodax/sdk 2.0.0-rc.11 → 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: {
@@ -3052,9 +3075,9 @@ var spokeChainConfig = {
3052
3075
  supportedTokens: bitcoinSupportedTokens,
3053
3076
  radfi: {
3054
3077
  walletMode: "TRADING",
3055
- apiUrl: "https://api.radfi.co/api",
3078
+ apiUrl: "https://api.bound.exchange/api",
3056
3079
  apiKey: "",
3057
- umsUrl: "https://ums.radfi.co/api",
3080
+ umsUrl: "https://api.ums.bound.exchange/api",
3058
3081
  accessToken: "",
3059
3082
  refreshToken: ""
3060
3083
  },
@@ -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,
@@ -3896,6 +3956,9 @@ function detectBitcoinAddressType(address) {
3896
3956
  return "P2PKH";
3897
3957
  throw new Error(`Unknown Bitcoin address type: ${address}`);
3898
3958
  }
3959
+ function usesBip322MessageSigning(addressType) {
3960
+ return addressType === "P2WPKH" || addressType === "P2TR";
3961
+ }
3899
3962
  var BTC_WALLET_ADDRESS_TYPES = ["taproot", "segwit"];
3900
3963
  var BTC_ADDRESS_TYPES = ["P2PKH", "P2SH", "P2WPKH", "P2TR"];
3901
3964
 
@@ -4013,7 +4076,7 @@ function isValidWalletProviderForChainKey(chainKey, walletProvider) {
4013
4076
  }
4014
4077
 
4015
4078
  // ../types/dist/index.js
4016
- var CONFIG_VERSION = 209;
4079
+ var CONFIG_VERSION = 211;
4017
4080
  function isEvmSpokeChainConfig(value) {
4018
4081
  return typeof value === "object" && value !== null && value.chain.type === "EVM" && value.chain.key !== HUB_CHAIN_KEY;
4019
4082
  }
@@ -4440,7 +4503,7 @@ async function waitUntilIntentExecuted(payload) {
4440
4503
  }
4441
4504
  async function relayTxAndWaitPacket(params) {
4442
4505
  try {
4443
- const { srcTxHash, data, chainKey, relayerApiEndpoint, timeout = DEFAULT_RELAY_TX_TIMEOUT } = params;
4506
+ const { srcTxHash, data, chainKey, relayerApiEndpoint, timeout = DEFAULT_RELAY_TX_TIMEOUT, pollTxHash } = params;
4444
4507
  const intentRelayChainId = getIntentRelayChainId(chainKey).toString();
4445
4508
  const isSplitTxChain = isSolanaChainKeyType(chainKey) || isBitcoinChainKeyType(chainKey);
4446
4509
  invariant(!isSplitTxChain || data !== void 0, "Data is required for Solana and Bitcoin chain keys");
@@ -4459,7 +4522,9 @@ async function relayTxAndWaitPacket(params) {
4459
4522
  if (!submitResult.ok) return submitResult;
4460
4523
  return await waitUntilIntentExecuted({
4461
4524
  intentRelayChainId,
4462
- srcTxHash,
4525
+ // The relay may track the packet under a different id than the submit tx_hash (Bitcoin
4526
+ // on-demand: submit "withdraw", poll the derived `od:<hash>`). Defaults to the submit id.
4527
+ srcTxHash: pollTxHash ?? srcTxHash,
4463
4528
  timeout,
4464
4529
  apiUrl: relayerApiEndpoint
4465
4530
  });
@@ -13298,11 +13363,43 @@ function mergeSodaxConfig(base2, override) {
13298
13363
  return merged;
13299
13364
  }
13300
13365
 
13366
+ // src/shared/logger.ts
13367
+ var consoleLogger = {
13368
+ debug: (message, data) => data ? console.debug(message, data) : console.debug(message),
13369
+ info: (message, data) => data ? console.info(message, data) : console.info(message),
13370
+ warn: (message, data) => data ? console.warn(message, data) : console.warn(message),
13371
+ error: (message, error, data) => {
13372
+ if (error !== void 0 && data !== void 0) console.error(message, error, data);
13373
+ else if (error !== void 0) console.error(message, error);
13374
+ else console.error(message);
13375
+ }
13376
+ };
13377
+ var silentLogger = {
13378
+ debug: () => {
13379
+ },
13380
+ info: () => {
13381
+ },
13382
+ warn: () => {
13383
+ },
13384
+ error: () => {
13385
+ }
13386
+ };
13387
+ function resolveLogger(option) {
13388
+ if (option === void 0 || option === "console") return consoleLogger;
13389
+ if (option === "silent") return silentLogger;
13390
+ return option;
13391
+ }
13392
+
13301
13393
  // src/shared/config/ConfigService.ts
13302
13394
  var ConfigService = class {
13303
13395
  sodax;
13304
13396
  api;
13305
13397
  userConfig;
13398
+ /**
13399
+ * SDK log sink. Resolved once at construction and kept independent of {@link sodax} so that
13400
+ * {@link initialize}'s dynamic-config swap never clobbers it. Read by services via `config.logger`.
13401
+ */
13402
+ logger;
13306
13403
  initialized = false;
13307
13404
  // data structures for quick lookup
13308
13405
  supportedHubAssetsSet;
@@ -13314,10 +13411,11 @@ var ConfigService = class {
13314
13411
  stakedATokenAddressesSet;
13315
13412
  chainToSupportedTokenAddressMap;
13316
13413
  hubAssetToXTokenMap;
13317
- constructor({ api, config, userConfig }) {
13414
+ constructor({ api, config, userConfig, logger }) {
13318
13415
  this.api = api;
13319
13416
  this.sodax = config;
13320
13417
  this.userConfig = userConfig;
13418
+ this.logger = logger ?? resolveLogger(void 0);
13321
13419
  this.loadSodaxConfigDataStructures(config);
13322
13420
  }
13323
13421
  async initialize() {
@@ -13326,7 +13424,7 @@ var ConfigService = class {
13326
13424
  if (!result.ok) return result;
13327
13425
  const response = result.value;
13328
13426
  if (!response.version || response.version < CONFIG_VERSION) {
13329
- console.warn(
13427
+ this.logger.warn(
13330
13428
  `Dynamic config version is less than the current version, resorting to the default one. Current version: ${CONFIG_VERSION}, response version: ${response.version}`
13331
13429
  );
13332
13430
  } else {
@@ -14484,7 +14582,7 @@ var RadfiProvider = class {
14484
14582
  }
14485
14583
  }
14486
14584
  /**
14487
- * Authenticate with Radfi: BIP322-sign a login message, then call the Radfi API.
14585
+ * Authenticate with Bound Exchange: BIP322-sign a login message, then call the Bound Exchange API.
14488
14586
  * Returns accessToken, refreshToken, and tradingAddress.
14489
14587
  */
14490
14588
  async authenticateWithWallet(walletProvider, cachedPublicKey) {
@@ -14500,15 +14598,14 @@ var RadfiProvider = class {
14500
14598
  throw new Error("Failed to retrieve public key from wallet. Please unlock your wallet and try again.");
14501
14599
  }
14502
14600
  const message = `${Date.now()}`;
14503
- const addressType = detectBitcoinAddressType(address);
14504
- const signature = addressType === "P2WPKH" || addressType === "P2TR" ? await walletProvider.signBip322Message(message) : await walletProvider.signEcdsaMessage(message);
14601
+ const signature = usesBip322MessageSigning(detectBitcoinAddressType(address)) ? await walletProvider.signBip322Message(message) : await walletProvider.signEcdsaMessage(message);
14505
14602
  const result = await this.authenticate({ message, signature, address, publicKey });
14506
14603
  this.setRadfiAccessToken(result.accessToken, result.refreshToken);
14507
14604
  return { ...result, publicKey };
14508
14605
  }
14509
14606
  /**
14510
- * Ensure a valid Radfi access token is set on this provider.
14511
- * If a token exists, validates it via the Radfi API.
14607
+ * Ensure a valid Bound Exchange access token is set on this provider.
14608
+ * If a token exists, validates it via the Bound Exchange API.
14512
14609
  * If invalid, tries refreshing with the refresh token first.
14513
14610
  * If refresh also fails, falls back to full re-authentication (BIP322 sign).
14514
14611
  */
@@ -14541,7 +14638,7 @@ var RadfiProvider = class {
14541
14638
  });
14542
14639
  if (!res.ok) {
14543
14640
  const err = await res.json();
14544
- throw new RadfiApiError(res.status, err, "Radfi authentication failed");
14641
+ throw new RadfiApiError(res.status, err, "Bound Exchange authentication failed");
14545
14642
  }
14546
14643
  return res.json().then((r) => ({
14547
14644
  accessToken: r.data?.accessToken ?? "",
@@ -14633,12 +14730,20 @@ var RadfiProvider = class {
14633
14730
  }
14634
14731
  })
14635
14732
  });
14636
- if (!res.ok) {
14637
- const err = await res.json();
14638
- throw new RadfiApiError(res.status, err, "Radfi transaction request failed");
14733
+ const body = await res.json();
14734
+ if (!res.ok || !body?.data) {
14735
+ throw new RadfiApiError(res.status, body, "Bound Exchange transaction request failed");
14639
14736
  }
14640
- return res.json().then((r) => r.data);
14737
+ return body.data;
14641
14738
  }
14739
+ /**
14740
+ * Co-sign and broadcast a `sodax-withdraw` deposit via the Bound Exchange API.
14741
+ *
14742
+ * `relayData` ({ address, payload }) is the same `RelayExtraData` the SDK returns from
14743
+ * `createIntent()` / money-market supply etc. It is optional and non-breaking: when supplied,
14744
+ * the Bound Exchange backend persists it so it can auto-resubmit the intent relay if the relay
14745
+ * gets stuck (otherwise a stuck relay eventually refunds instead of completing the swap).
14746
+ */
14642
14747
  async requestRadfiSignature(params, accessToken) {
14643
14748
  const res = await this.request("/sodax/transaction/sign", {
14644
14749
  method: "POST",
@@ -14652,7 +14757,7 @@ var RadfiProvider = class {
14652
14757
  });
14653
14758
  if (!res.ok) {
14654
14759
  const err = await res.json();
14655
- throw new RadfiApiError(res.status, err, "Radfi signature request failed");
14760
+ throw new RadfiApiError(res.status, err, "Bound Exchange signature request failed");
14656
14761
  }
14657
14762
  return res.json().then((r) => r.data.txId);
14658
14763
  }
@@ -14676,7 +14781,7 @@ var RadfiProvider = class {
14676
14781
  return res.json();
14677
14782
  }
14678
14783
  /**
14679
- * Build a renew-utxo transaction via the Radfi API.
14784
+ * Build a renew-utxo transaction via the Bound Exchange API.
14680
14785
  * Returns a PSBT that needs to be signed by the user.
14681
14786
  */
14682
14787
  async buildRenewUtxoTransaction(params, accessToken) {
@@ -14700,8 +14805,8 @@ var RadfiProvider = class {
14700
14805
  return res.json().then((r) => r.data);
14701
14806
  }
14702
14807
  /**
14703
- * Sign and broadcast a renew-utxo transaction via the Radfi API.
14704
- * The user signs the PSBT first, then Radfi co-signs and broadcasts.
14808
+ * Sign and broadcast a renew-utxo transaction via the Bound Exchange API.
14809
+ * The user signs the PSBT first, then Bound Exchange co-signs and broadcasts.
14705
14810
  */
14706
14811
  async signAndBroadcastRenewUtxo(params, accessToken) {
14707
14812
  const res = await this.request("/transactions/sign", {
@@ -14742,7 +14847,7 @@ var RadfiProvider = class {
14742
14847
  return res.json().then((r) => r.data);
14743
14848
  }
14744
14849
  /**
14745
- * Sign and broadcast a withdraw transaction via Radfi.
14850
+ * Sign and broadcast a withdraw transaction via Bound Exchange.
14746
14851
  */
14747
14852
  async signAndBroadcastWithdraw(params, accessToken) {
14748
14853
  const res = await this.request("/transactions/sign", {
@@ -14796,7 +14901,20 @@ var RadfiProvider = class {
14796
14901
  };
14797
14902
 
14798
14903
  // src/shared/entities/btc/btc-utils.ts
14799
- function estimateBitcoinTxSize(inputCount, outputCount, addressType) {
14904
+ var BITCOIN_FEE_SAFETY_VBYTES = 20;
14905
+ function calcOpReturnOutputVbytes(payloadByteLength) {
14906
+ let scriptSize;
14907
+ if (payloadByteLength <= 75) {
14908
+ scriptSize = 3 + payloadByteLength;
14909
+ } else if (payloadByteLength <= 255) {
14910
+ scriptSize = 4 + payloadByteLength;
14911
+ } else {
14912
+ scriptSize = 5 + payloadByteLength;
14913
+ }
14914
+ const scriptLenVarint = scriptSize <= 252 ? 1 : 3;
14915
+ return 8 + scriptLenVarint + scriptSize;
14916
+ }
14917
+ function estimateBitcoinTxSize(inputCount, outputCount, addressType, opReturnOutputVbytes = 44) {
14800
14918
  let inputWeight;
14801
14919
  switch (addressType) {
14802
14920
  case "P2PKH":
@@ -14812,7 +14930,9 @@ function estimateBitcoinTxSize(inputCount, outputCount, addressType) {
14812
14930
  inputWeight = 68;
14813
14931
  break;
14814
14932
  }
14815
- return Math.ceil(10.5 + 44 + inputCount * inputWeight + outputCount * 31);
14933
+ return Math.ceil(
14934
+ 10.5 + opReturnOutputVbytes + BITCOIN_FEE_SAFETY_VBYTES + inputCount * inputWeight + outputCount * 31
14935
+ );
14816
14936
  }
14817
14937
  function encodeBtcPayloadToBytes(payload) {
14818
14938
  return JSON.stringify({
@@ -14829,6 +14949,10 @@ function normalizePsbtToBase64(signedPsbt) {
14829
14949
  const isHex = /^[0-9a-fA-F]+$/.test(signedPsbt);
14830
14950
  return isHex ? Buffer.from(signedPsbt, "hex").toString("base64") : signedPsbt;
14831
14951
  }
14952
+ function normalizeSignatureToBase64(signature) {
14953
+ const isHex = /^[0-9a-fA-F]+$/.test(signature);
14954
+ return isHex ? Buffer.from(signature, "hex").toString("base64") : signature;
14955
+ }
14832
14956
 
14833
14957
  // src/shared/services/spoke/BitcoinSpokeService.ts
14834
14958
  bitcoinjsLib.initEccLib(ecc__namespace);
@@ -14937,7 +15061,7 @@ var BitcoinSpokeService = class {
14937
15061
  return BigInt(totalBalance);
14938
15062
  }
14939
15063
  /**
14940
- * Fund the Radfi trading wallet by sending BTC from the user's personal wallet
15064
+ * Fund the Bound Exchange trading wallet by sending BTC from the user's personal wallet
14941
15065
  *
14942
15066
  * @param {bigint} amount - Amount in satoshis to send
14943
15067
  * @param {BitcoinSpokeProvider} spokeProvider - The Bitcoin spoke provider (must have signing capability)
@@ -14961,10 +15085,28 @@ var BitcoinSpokeService = class {
14961
15085
  async sendMessage(params) {
14962
15086
  return await this.encodeWithdrawalData(params);
14963
15087
  }
15088
+ /**
15089
+ * Build the relay submit/poll identity for an on-demand action (borrow/withdraw).
15090
+ *
15091
+ * Bitcoin borrow/withdraw are on-demand: there is no broadcast transaction — the spoke result is
15092
+ * the signed payload JSON produced by {@link encodeWithdrawalData}/{@link sendMessage}. The relay
15093
+ * accepts the submit under the literal `withdraw` tx_hash with the signed payload (as a JSON object)
15094
+ * in `data`, then tracks the resulting packet under a derived id: `od:` + keccak256 of the ASCII
15095
+ * `payload_hex` string (hash the hex characters, not the decoded bytes). Polling must use that
15096
+ * derived id (`pollTxHash`), not `withdraw`.
15097
+ *
15098
+ * @param tx - The JSON-stringified signed payload returned by `sendMessage` / `encodeWithdrawalData`.
15099
+ */
15100
+ getOnDemandRelayIdentity(tx) {
15101
+ const data = JSON.parse(tx);
15102
+ const payloadHex = data.payload_hex.startsWith("0x") ? data.payload_hex.slice(2) : data.payload_hex;
15103
+ const pollTxHash = `od:${viem.keccak256(viem.stringToBytes(payloadHex)).slice(2)}`;
15104
+ return { srcTxHash: "withdraw", data, pollTxHash };
15105
+ }
14964
15106
  /**
14965
15107
  * Build a priority Bitcoin transaction with proper fee calculation
14966
15108
  */
14967
- async buildBitcoinTransaction(utxos, outputs, changeAddress, chainId, walletProvider, feeRate) {
15109
+ async buildBitcoinTransaction(utxos, outputs, changeAddress, chainId, walletProvider, feeRate, opReturnOutputVbytes) {
14968
15110
  const psbt = new bitcoinjsLib.Psbt({ network: this.getBtcNetwork(chainId) });
14969
15111
  const effectiveFeeRate = feeRate ?? await this.getFeeRateEstimate();
14970
15112
  const walletAddress = await walletProvider.getWalletAddress();
@@ -15027,7 +15169,7 @@ var BitcoinSpokeService = class {
15027
15169
  });
15028
15170
  }
15029
15171
  inputSum += utxo.value;
15030
- const estimatedSize = estimateBitcoinTxSize(psbt.inputCount, outputs.length, addressType);
15172
+ const estimatedSize = estimateBitcoinTxSize(psbt.inputCount, outputs.length, addressType, opReturnOutputVbytes);
15031
15173
  const estimatedFee = Math.ceil(effectiveFeeRate * estimatedSize);
15032
15174
  if (inputSum >= outputSum + estimatedFee + DUST_THRESHOLD) {
15033
15175
  break;
@@ -15039,8 +15181,13 @@ var BitcoinSpokeService = class {
15039
15181
  value: output.value
15040
15182
  });
15041
15183
  }
15042
- const sizeWithChange = estimateBitcoinTxSize(psbt.inputCount, outputs.length + 1, addressType);
15043
- const sizeWithoutChange = estimateBitcoinTxSize(psbt.inputCount, outputs.length, addressType);
15184
+ const sizeWithChange = estimateBitcoinTxSize(
15185
+ psbt.inputCount,
15186
+ outputs.length + 1,
15187
+ addressType,
15188
+ opReturnOutputVbytes
15189
+ );
15190
+ const sizeWithoutChange = estimateBitcoinTxSize(psbt.inputCount, outputs.length, addressType, opReturnOutputVbytes);
15044
15191
  const feeWithChange = Math.ceil(effectiveFeeRate * sizeWithChange);
15045
15192
  const feeWithoutChange = Math.ceil(effectiveFeeRate * sizeWithoutChange);
15046
15193
  let change = inputSum - outputSum - feeWithChange;
@@ -15109,7 +15256,12 @@ var BitcoinSpokeService = class {
15109
15256
  return await this.radfi.requestRadfiSignature(
15110
15257
  {
15111
15258
  userAddress: from,
15112
- signedBase64Tx
15259
+ signedBase64Tx,
15260
+ // Forward the relay identity ({ hub wallet address, full payload }) so the Bound Exchange
15261
+ // backend can auto-resubmit the intent relay if it gets stuck. `to` is the hub wallet
15262
+ // (relayData.address) and `data` is the full payload (relayData.payload) — the same pair
15263
+ // feature services return as `relayData` from createIntent()/supply().
15264
+ relayData: { address: params.to, payload: data }
15113
15265
  },
15114
15266
  accessToken
15115
15267
  );
@@ -15119,7 +15271,11 @@ var BitcoinSpokeService = class {
15119
15271
  "Raw mode is not supported for normal Bitcoin deposits. Use TRADING wallet mode for raw transactions."
15120
15272
  );
15121
15273
  }
15122
- const utxos = await this.fetchUTXOs(from);
15274
+ const [allUtxos, mempoolSpent] = await Promise.all([
15275
+ this.fetchUTXOs(from),
15276
+ this.fetchMempoolSpentOutpoints(from)
15277
+ ]);
15278
+ const utxos = allUtxos.filter((u) => !mempoolSpent.has(`${u.txid}:${u.vout}`));
15123
15279
  if (!utxos?.length) {
15124
15280
  throw new Error("No UTXOs available for deposit");
15125
15281
  }
@@ -15134,7 +15290,7 @@ var BitcoinSpokeService = class {
15134
15290
  );
15135
15291
  return await this.signAndBroadcastTransaction(depositPsbt, params.walletProvider);
15136
15292
  } catch (error) {
15137
- console.error("Error during deposit:", error);
15293
+ this.config.logger.error("Error during deposit", error);
15138
15294
  throw error;
15139
15295
  }
15140
15296
  }
@@ -15142,22 +15298,37 @@ var BitcoinSpokeService = class {
15142
15298
  * Build deposit PSBT with embedded cross-chain data
15143
15299
  */
15144
15300
  async buildDepositPsbt(walletAddress, walletProvider, srcChainKey, token, amount, data, utxos) {
15145
- const assetManagerAddress = this.config.getChainConfig(srcChainKey).addresses.assetManager;
15146
- if (token.toLocaleLowerCase() === "btc") {
15301
+ const chainConfig = this.config.getChainConfig(srcChainKey);
15302
+ const assetManagerAddress = chainConfig.addresses.assetManager;
15303
+ const normalizedToken = token.toLocaleLowerCase();
15304
+ const nativeBtcTokens = new Set(
15305
+ ["btc", chainConfig.nativeToken, chainConfig.supportedTokens.BTC?.address].filter((value) => !!value).map((value) => value.toLocaleLowerCase())
15306
+ );
15307
+ const isNativeBtc = nativeBtcTokens.has(normalizedToken);
15308
+ if (isNativeBtc) {
15309
+ const OP_RETURN = bitcoinjsLib.opcodes.OP_RETURN;
15310
+ const OP_12 = bitcoinjsLib.opcodes.OP_12;
15311
+ if (OP_RETURN === void 0 || OP_12 === void 0) {
15312
+ throw new Error("bitcoinjs-lib opcodes OP_RETURN or OP_12 are undefined");
15313
+ }
15314
+ const OP_RADFI_SODAX_DATA = 49;
15315
+ const payload = Buffer.concat([Buffer.from([OP_RADFI_SODAX_DATA]), Buffer.from(data.slice(2), "hex")]);
15316
+ const opReturnOutputVbytes = calcOpReturnOutputVbytes(payload.length);
15147
15317
  const outputs = [
15148
15318
  {
15149
15319
  address: assetManagerAddress,
15150
15320
  value: Number(amount)
15151
15321
  }
15152
15322
  ];
15153
- const psbt = await this.buildBitcoinTransaction(utxos, outputs, walletAddress, srcChainKey, walletProvider);
15154
- const OP_RADFI_SODAX_DATA = 49;
15155
- const payload = Buffer.concat([Buffer.from([OP_RADFI_SODAX_DATA]), Buffer.from(data.slice(2), "hex")]);
15156
- const OP_RETURN = bitcoinjsLib.opcodes.OP_RETURN;
15157
- const OP_12 = bitcoinjsLib.opcodes.OP_12;
15158
- if (OP_RETURN === void 0 || OP_12 === void 0) {
15159
- throw new Error("bitcoinjs-lib opcodes OP_RETURN or OP_12 are undefined");
15160
- }
15323
+ const psbt = await this.buildBitcoinTransaction(
15324
+ utxos,
15325
+ outputs,
15326
+ walletAddress,
15327
+ srcChainKey,
15328
+ walletProvider,
15329
+ void 0,
15330
+ opReturnOutputVbytes
15331
+ );
15161
15332
  const compiledScript = bitcoinjsLib.script.compile([OP_RETURN, OP_12, payload]);
15162
15333
  psbt.addOutput({
15163
15334
  script: compiledScript,
@@ -15177,6 +15348,27 @@ var BitcoinSpokeService = class {
15177
15348
  }
15178
15349
  return await response.json();
15179
15350
  }
15351
+ /**
15352
+ * Returns the set of "txid:vout" outpoints currently being spent by
15353
+ * unconfirmed transactions in the mempool for the given address.
15354
+ * Used to prevent double-spend when building a new PSBT.
15355
+ */
15356
+ async fetchMempoolSpentOutpoints(address) {
15357
+ try {
15358
+ const response = await fetch(`${this.rpcUrl}/address/${address}/txs/mempool`);
15359
+ if (!response.ok) return /* @__PURE__ */ new Set();
15360
+ const mempoolTxs = await response.json();
15361
+ const spent = /* @__PURE__ */ new Set();
15362
+ for (const tx of mempoolTxs) {
15363
+ for (const input of tx.vin) {
15364
+ spent.add(`${input.txid}:${input.vout}`);
15365
+ }
15366
+ }
15367
+ return spent;
15368
+ } catch {
15369
+ return /* @__PURE__ */ new Set();
15370
+ }
15371
+ }
15180
15372
  /**
15181
15373
  * Fetch raw transaction hex
15182
15374
  */
@@ -15192,7 +15384,7 @@ var BitcoinSpokeService = class {
15192
15384
  let srcAddress = from;
15193
15385
  const addressType = detectBitcoinAddressType(from);
15194
15386
  if (walletMode === "TRADING") {
15195
- srcAddress = await this.radfi.getTradingWallet(srcAddress).then((res) => res.tradingAddress).catch(() => srcAddress);
15387
+ srcAddress = (await this.radfi.getTradingWallet(srcAddress)).tradingAddress;
15196
15388
  }
15197
15389
  const payload = {
15198
15390
  src_address: srcAddress,
@@ -15211,8 +15403,12 @@ var BitcoinSpokeService = class {
15211
15403
  if (params.raw === true) {
15212
15404
  return JSON.stringify(onDemandWithdraw);
15213
15405
  }
15214
- const signature = await params.walletProvider.signEcdsaMessage(orderedPayload);
15215
- onDemandWithdraw.signature = signature;
15406
+ if (!params.walletProvider.getPublicKey) {
15407
+ throw new Error("Wallet provider does not support getPublicKey");
15408
+ }
15409
+ const rawSignature = usesBip322MessageSigning(addressType) ? await params.walletProvider.signBip322Message(orderedPayload) : await params.walletProvider.signEcdsaMessage(orderedPayload);
15410
+ onDemandWithdraw.signature = normalizeSignatureToBase64(rawSignature);
15411
+ onDemandWithdraw.public_key = await params.walletProvider.getPublicKey();
15216
15412
  return JSON.stringify(onDemandWithdraw);
15217
15413
  }
15218
15414
  /**
@@ -15220,8 +15416,14 @@ var BitcoinSpokeService = class {
15220
15416
  */
15221
15417
  async signAndBroadcastTransaction(psbt, walletProvider) {
15222
15418
  const psbtBase64 = typeof psbt === "string" ? psbt : psbt.toBase64();
15223
- const signedPsbtHex = await walletProvider.signTransaction(psbtBase64);
15224
- const txHash = await this.broadcastTransaction(signedPsbtHex);
15419
+ const signedRaw = await walletProvider.signTransaction(psbtBase64, false);
15420
+ const signedPsbt = bitcoinjsLib.Psbt.fromBase64(normalizePsbtToBase64(signedRaw));
15421
+ try {
15422
+ signedPsbt.finalizeAllInputs();
15423
+ } catch {
15424
+ }
15425
+ const txHex = signedPsbt.extractTransaction().toHex();
15426
+ const txHash = await this.broadcastTransaction(txHex);
15225
15427
  return txHash;
15226
15428
  }
15227
15429
  /**
@@ -17460,7 +17662,7 @@ function base64Decode(str) {
17460
17662
  }
17461
17663
  var encoder = new TextEncoder();
17462
17664
  var decoder = new TextDecoder();
17463
- function stringToBytes(str) {
17665
+ function stringToBytes2(str) {
17464
17666
  return encoder.encode(str);
17465
17667
  }
17466
17668
  function bytesToString(bytes) {
@@ -20857,7 +21059,7 @@ var JsonRpcProvider = class {
20857
21059
  };
20858
21060
  }
20859
21061
  async viewContractState({ contractId, prefix: prefix2, blockQuery = { finality: DEFAULT_FINALITY } }) {
20860
- const prefixBase64 = base64Encode(stringToBytes(prefix2 || ""));
21062
+ const prefixBase64 = base64Encode(stringToBytes2(prefix2 || ""));
20861
21063
  let reference;
20862
21064
  if ("blockId" in blockQuery) {
20863
21065
  reference = { block_id: blockQuery.blockId };
@@ -20885,7 +21087,7 @@ var JsonRpcProvider = class {
20885
21087
  }
20886
21088
  }
20887
21089
  async callFunctionRaw({ contractId, method, args, blockQuery = { finality: DEFAULT_FINALITY } }) {
20888
- const argsBytes = args instanceof Uint8Array ? args : stringToBytes(JSON.stringify(args));
21090
+ const argsBytes = args instanceof Uint8Array ? args : stringToBytes2(JSON.stringify(args));
20889
21091
  const argsBase64 = base64Encode(argsBytes);
20890
21092
  if ("blockId" in blockQuery) {
20891
21093
  return this.query({
@@ -21151,6 +21353,7 @@ var JsonRpcProvider = class {
21151
21353
 
21152
21354
  // src/shared/services/spoke/NearSpokeService.ts
21153
21355
  var NEAR_DEFAULT_GAS = BigInt("300000000000000");
21356
+ var NEAR_STORAGE_DEPOSIT = BigInt("1250000000000000000000");
21154
21357
  var NearSpokeService = class {
21155
21358
  config;
21156
21359
  rpcProvider;
@@ -21200,7 +21403,7 @@ var NearSpokeService = class {
21200
21403
  token: Array.from(Buffer.from(fillData.token, "utf-8"))
21201
21404
  };
21202
21405
  }
21203
- async fillIntent(fromInfo, fillData, deposit = BigInt("0"), gas = BigInt("300000000000000")) {
21406
+ async fillIntent(fromInfo, fillData, deposit = BigInt("1"), gas = BigInt("300000000000000")) {
21204
21407
  const intentFiller = this.config.getChainConfig(fromInfo.srcChainKey).addresses.intentFiller;
21205
21408
  if (isNativeToken(fromInfo.srcChainKey, fillData.token)) {
21206
21409
  deposit = BigInt(fillData.amount);
@@ -21274,7 +21477,8 @@ var NearSpokeService = class {
21274
21477
  data: inputParams.data
21275
21478
  })
21276
21479
  },
21277
- deposit: BigInt("0"),
21480
+ // NEP-141 requires exactly 1 yoctoNEAR attached to ft_transfer_call.
21481
+ deposit: BigInt("1"),
21278
21482
  gas: NEAR_DEFAULT_GAS
21279
21483
  }
21280
21484
  };
@@ -21305,6 +21509,52 @@ var NearSpokeService = class {
21305
21509
  }
21306
21510
  return BigInt(bal);
21307
21511
  }
21512
+ /**
21513
+ * Whether `accountId` is storage-registered on a NEP-141 `token` contract. NEP-141 requires an
21514
+ * account to pay a one-time storage bond before it can receive (hold a balance of) the token, so
21515
+ * this gates any leg that delivers a token to a user on NEAR (swap output on NEAR, bridge into
21516
+ * NEAR, money-market borrow/withdraw to NEAR).
21517
+ *
21518
+ * Native NEAR is not a NEP-141 token and has no storage registration, so this returns `true` for
21519
+ * the native token. The view goes through {@link queryContract}, i.e. the configurable RPC from
21520
+ * chain config — a custom `rpcUrl` passed to the SDK is honoured.
21521
+ */
21522
+ async isStorageRegistered(token, accountId) {
21523
+ if (isNativeToken(ChainKeys.NEAR_MAINNET, token)) {
21524
+ return true;
21525
+ }
21526
+ const balance = await this.queryContract(token, "storage_balance_of", { account_id: accountId });
21527
+ return balance != null;
21528
+ }
21529
+ /**
21530
+ * Build (and, unless `raw`, submit) a NEP-141 `storage_deposit` registration for `accountId` on
21531
+ * the `token` contract. Call this when {@link isStorageRegistered} is `false` before a token is
21532
+ * delivered to the account on NEAR.
21533
+ *
21534
+ * Native NEAR has no storage registration — passing the native token throws.
21535
+ *
21536
+ * @param params.deposit Storage bond to attach; defaults to {@link NEAR_STORAGE_DEPOSIT}
21537
+ * (0.00125 NEAR). Override per token if its `storage_balance_bounds.min` differs.
21538
+ */
21539
+ async registerStorage(params) {
21540
+ if (isNativeToken(ChainKeys.NEAR_MAINNET, params.token)) {
21541
+ throw new Error("[NearSpokeService.registerStorage] Native NEAR has no NEP-141 storage registration.");
21542
+ }
21543
+ const tx = {
21544
+ signerId: params.accountId,
21545
+ params: {
21546
+ contractId: params.token,
21547
+ method: "storage_deposit",
21548
+ args: { account_id: params.accountId, registration_only: true },
21549
+ deposit: params.deposit ?? NEAR_STORAGE_DEPOSIT,
21550
+ gas: NEAR_DEFAULT_GAS
21551
+ }
21552
+ };
21553
+ if (params.raw === true) {
21554
+ return tx;
21555
+ }
21556
+ return params.walletProvider.signAndSubmitTxn(tx);
21557
+ }
21308
21558
  /**
21309
21559
  * Sends a message to the hub chain.
21310
21560
  * @param {SendMessageParams} params - Includes dstChainKey, the chain key of the hub chain.
@@ -22105,17 +22355,11 @@ var SuiSpokeService = class {
22105
22355
  ]
22106
22356
  });
22107
22357
  if (params.raw === true) {
22108
- tx.setSender(from);
22109
- const transactionRaw = await tx.build({
22110
- client: this.publicClient,
22111
- onlyTransactionKind: true
22112
- });
22113
- const transactionRawBase64String = Buffer.from(transactionRaw).toString("base64");
22114
22358
  return {
22115
22359
  from,
22116
22360
  to: `${assetManager.packageId}::${assetManager.moduleId}::transfer`,
22117
22361
  value: amount,
22118
- data: transactionRawBase64String
22362
+ data: tx.serialize()
22119
22363
  };
22120
22364
  }
22121
22365
  return params.walletProvider.signAndExecuteTxn(tx);
@@ -22135,17 +22379,11 @@ var SuiSpokeService = class {
22135
22379
  ]
22136
22380
  });
22137
22381
  if (params.raw === true) {
22138
- txb.setSender(from);
22139
- const transactionRaw = await txb.build({
22140
- client: this.publicClient,
22141
- onlyTransactionKind: true
22142
- });
22143
- const transactionRawBase64String = Buffer.from(transactionRaw).toString("base64");
22144
22382
  return {
22145
22383
  from,
22146
22384
  to: `${connection.packageId}::${connection.moduleId}::send_message_ua`,
22147
22385
  value: 0n,
22148
- data: transactionRawBase64String
22386
+ data: txb.serialize()
22149
22387
  };
22150
22388
  }
22151
22389
  return params.walletProvider.signAndExecuteTxn(txb);
@@ -22156,7 +22394,7 @@ var SuiSpokeService = class {
22156
22394
  * @returns {Promise<bigint>} The estimated computation cost.
22157
22395
  */
22158
22396
  async estimateGas({ tx }) {
22159
- const txb = transactions.Transaction.fromKind(tx.data);
22397
+ const txb = transactions.Transaction.from(tx.data);
22160
22398
  const result = await this.publicClient.devInspectTransactionBlock({
22161
22399
  sender: tx.from,
22162
22400
  transactionBlock: txb
@@ -22387,6 +22625,7 @@ var CustomStellarAccount = class {
22387
22625
  decrementSequenceNumber() {
22388
22626
  if (this.sequenceNumber > this.startingSequenceNumber) {
22389
22627
  this.sequenceNumber--;
22628
+ return;
22390
22629
  }
22391
22630
  throw new Error(
22392
22631
  `Sequence number cannot be decremented below the starting sequence number: ${this.startingSequenceNumber}`
@@ -22493,7 +22732,7 @@ var StellarSpokeService = class {
22493
22732
  );
22494
22733
  const assembledPriorityTx = stellarSdk.rpc.assembleTransaction(rawPriorityTx, simulation).build();
22495
22734
  if (params.raw) {
22496
- const transactionXdr = rawPriorityTx.toXDR();
22735
+ const transactionXdr = assembledPriorityTx.toXDR();
22497
22736
  return {
22498
22737
  from,
22499
22738
  to: this.config.getChainConfig(srcChainKey).addresses.assetManager,
@@ -22512,13 +22751,13 @@ var StellarSpokeService = class {
22512
22751
  );
22513
22752
  return `${hash}`;
22514
22753
  } catch (error) {
22515
- console.error("Error during sendMessage:", error);
22754
+ this.config.logger.error("Error during sendMessage", error);
22516
22755
  throw error;
22517
22756
  }
22518
22757
  }
22519
22758
  handleSendTransactionError(response) {
22520
22759
  if (response.status === "ERROR") {
22521
- console.error(JSON.stringify(response, null, 2));
22760
+ this.config.logger.error(JSON.stringify(response, null, 2));
22522
22761
  throw new Error(JSON.stringify(response, null, 2));
22523
22762
  }
22524
22763
  return response;
@@ -22610,7 +22849,7 @@ var StellarSpokeService = class {
22610
22849
  );
22611
22850
  const assembledPriorityTx = stellarSdk.rpc.assembleTransaction(rawPriorityTx, simulation).build();
22612
22851
  if (params.raw) {
22613
- const transactionXdr = rawPriorityTx.toXDR();
22852
+ const transactionXdr = assembledPriorityTx.toXDR();
22614
22853
  return {
22615
22854
  from,
22616
22855
  to: this.config.getChainConfig(srcChainKey).addresses.assetManager,
@@ -22629,7 +22868,7 @@ var StellarSpokeService = class {
22629
22868
  );
22630
22869
  return `${hash}`;
22631
22870
  } catch (error) {
22632
- console.error("Error during deposit:", error);
22871
+ this.config.logger.error("Error during deposit", error);
22633
22872
  throw error;
22634
22873
  }
22635
22874
  }
@@ -22657,7 +22896,7 @@ var StellarSpokeService = class {
22657
22896
  (balance2) => "limit" in balance2 && "balance" in balance2 && "asset_code" in balance2 && trustlineConfig.assetCode.toLowerCase() === balance2.asset_code?.toLowerCase() && "asset_issuer" in balance2 && trustlineConfig.assetIssuer.toLowerCase() === balance2.asset_issuer?.toLowerCase()
22658
22897
  );
22659
22898
  if (!tokenBalance) {
22660
- console.error(`No token balances found for token: ${token}`);
22899
+ this.config.logger.error(`No token balances found for token: ${token}`);
22661
22900
  return false;
22662
22901
  }
22663
22902
  const limit = parseToStroops(tokenBalance.limit);
@@ -22706,7 +22945,7 @@ var StellarSpokeService = class {
22706
22945
  const hash = await this.signAndSendTransaction(walletProvider, transaction);
22707
22946
  return `${hash}`;
22708
22947
  } catch (error) {
22709
- console.error("Error during requestTrustline:", error);
22948
+ this.config.logger.error("Error during requestTrustline", error);
22710
22949
  throw error;
22711
22950
  }
22712
22951
  }
@@ -23565,7 +23804,7 @@ var SpokeService = class _SpokeService {
23565
23804
  }
23566
23805
  default: {
23567
23806
  const exhaustiveCheck = chainType;
23568
- console.log(exhaustiveCheck);
23807
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
23569
23808
  throw new Error(`[getSpokeService] Invalid chain type. Valid chain types: ${ChainTypeArr.join(", ")}`);
23570
23809
  }
23571
23810
  }
@@ -23738,7 +23977,7 @@ var SpokeService = class _SpokeService {
23738
23977
  }
23739
23978
  default: {
23740
23979
  const exhaustiveCheck = chainType;
23741
- console.log(exhaustiveCheck);
23980
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
23742
23981
  return {
23743
23982
  ok: false,
23744
23983
  error: new Error(`[estimateGas] Invalid chain type. Valid chain types: ${ChainTypeArr.join(", ")}`)
@@ -23845,17 +24084,17 @@ var SpokeService = class _SpokeService {
23845
24084
  args: [params.srcChainId, params.srcAddress, params.payload]
23846
24085
  })
23847
24086
  });
23848
- console.warn("simulateRecvMessage did not revert as expected", { result });
24087
+ this.config.logger.warn("simulateRecvMessage did not revert as expected", { result });
23849
24088
  return {
23850
24089
  ok: false,
23851
24090
  error: new Error('Function should have reverted with "Simulation completed"')
23852
24091
  };
23853
24092
  } catch (error) {
23854
24093
  if (error instanceof Error && error.message?.includes("Simulation completed")) {
23855
- console.warn("simulateRecvMessage completed successfully with expected revert");
24094
+ this.config.logger.warn("simulateRecvMessage completed successfully with expected revert");
23856
24095
  return { ok: true, value: true };
23857
24096
  }
23858
- console.error("simulateRecvMessage failed with unexpected error:", error);
24097
+ this.config.logger.error("simulateRecvMessage failed with unexpected error", error);
23859
24098
  return { ok: false, error };
23860
24099
  }
23861
24100
  }
@@ -23940,7 +24179,7 @@ var SpokeService = class _SpokeService {
23940
24179
  }
23941
24180
  default: {
23942
24181
  const exhaustiveCheck = chainType;
23943
- console.log(exhaustiveCheck);
24182
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
23944
24183
  return {
23945
24184
  ok: false,
23946
24185
  error: new Error(`[deposit] Invalid chain type. Valid chain types: ${ChainTypeArr.join(", ")}`)
@@ -24017,7 +24256,7 @@ var SpokeService = class _SpokeService {
24017
24256
  }
24018
24257
  default: {
24019
24258
  const exhaustiveCheck = chainType;
24020
- console.log(exhaustiveCheck);
24259
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
24021
24260
  return {
24022
24261
  ok: false,
24023
24262
  error: new Error(`[getDeposit] Invalid chain type. Valid chain types: ${ChainTypeArr.join(", ")}`)
@@ -24126,7 +24365,7 @@ var SpokeService = class _SpokeService {
24126
24365
  }
24127
24366
  default: {
24128
24367
  const exhaustiveCheck = chainType;
24129
- console.log(exhaustiveCheck);
24368
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
24130
24369
  return {
24131
24370
  ok: false,
24132
24371
  error: new Error(`[sendMessage] Invalid chain type. Valid chain types: ${ChainTypeArr.join(", ")}`)
@@ -24205,10 +24444,10 @@ var SpokeService = class _SpokeService {
24205
24444
  if (isSolanaChainKeyType(chainKey)) {
24206
24445
  const result = await this.solana.waitForTransactionReceipt({ txHash, chainKey });
24207
24446
  if (!result.ok || result.value.status !== "success") {
24208
- console.warn(
24447
+ this.config.logger.warn(
24209
24448
  `Solana verifyTxHash failed: ${!result.ok ? result.error : "error" in result.value ? result.value.error : "unknown"}`
24210
24449
  );
24211
- console.warn("Returning true to assume transaction exists on chain in future ");
24450
+ this.config.logger.warn("Returning true to assume transaction exists on chain in future ");
24212
24451
  return { ok: true, value: true };
24213
24452
  }
24214
24453
  return { ok: true, value: true };
@@ -24295,7 +24534,7 @@ var SpokeService = class _SpokeService {
24295
24534
  }
24296
24535
  default: {
24297
24536
  const exhaustiveCheck = chainType;
24298
- console.log(exhaustiveCheck);
24537
+ this.config.logger.debug("Unhandled exhaustive case", { value: exhaustiveCheck });
24299
24538
  return { ok: false, error: new Error(`waitForTransactionReceipt not supported for ${params.chainKey}`) };
24300
24539
  }
24301
24540
  }
@@ -25346,6 +25585,7 @@ var EvmHubProvider = class {
25346
25585
  };
25347
25586
 
25348
25587
  // src/swap/SolverApiService.ts
25588
+ var bigintReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
25349
25589
  var SolverApiService = class {
25350
25590
  constructor() {
25351
25591
  }
@@ -25414,13 +25654,16 @@ var SolverApiService = class {
25414
25654
  }
25415
25655
  };
25416
25656
  } catch (e) {
25417
- console.error(`[SolverApiService.getQuote] failed. Details: ${JSON.stringify(e)}`);
25657
+ configService.logger.error(
25658
+ "[SolverApiService.getQuote] failed",
25659
+ e instanceof Error ? e : new Error(JSON.stringify(e, bigintReplacer))
25660
+ );
25418
25661
  return {
25419
25662
  ok: false,
25420
25663
  error: {
25421
25664
  detail: {
25422
25665
  code: exports.SolverIntentErrorCode.UNKNOWN,
25423
- message: e ? JSON.stringify(e) : "Unknown error"
25666
+ message: e ? JSON.stringify(e, bigintReplacer) : "Unknown error"
25424
25667
  }
25425
25668
  }
25426
25669
  };
@@ -25437,7 +25680,7 @@ var SolverApiService = class {
25437
25680
  * @param config - Solver endpoint configuration.
25438
25681
  * @returns A `Result` containing `{ answer: 'OK', intent_hash: Hex }` on success.
25439
25682
  */
25440
- static async postExecution(request, config) {
25683
+ static async postExecution(request, config, logger = silentLogger) {
25441
25684
  try {
25442
25685
  const response = await retry(
25443
25686
  () => fetch(`${config.solverApiEndpoint}/execute`, {
@@ -25459,13 +25702,16 @@ var SolverApiService = class {
25459
25702
  value: await response.json()
25460
25703
  };
25461
25704
  } catch (e) {
25462
- console.error(`[SolverApiService.postExecution] failed. Details: ${JSON.stringify(e)}`);
25705
+ logger.error(
25706
+ "[SolverApiService.postExecution] failed",
25707
+ e instanceof Error ? e : new Error(JSON.stringify(e, bigintReplacer))
25708
+ );
25463
25709
  return {
25464
25710
  ok: false,
25465
25711
  error: {
25466
25712
  detail: {
25467
25713
  code: exports.SolverIntentErrorCode.UNKNOWN,
25468
- message: e ? JSON.stringify(e) : "Unknown error"
25714
+ message: e ? JSON.stringify(e, bigintReplacer) : "Unknown error"
25469
25715
  }
25470
25716
  }
25471
25717
  };
@@ -25480,7 +25726,7 @@ var SolverApiService = class {
25480
25726
  * `fill_tx_hash` is set only when `status === SolverIntentStatusCode.SOLVED (3)`.
25481
25727
  * @throws Invariant error if `intent_tx_hash` is empty (thrown before the async request).
25482
25728
  */
25483
- static async getStatus(request, config) {
25729
+ static async getStatus(request, config, logger = silentLogger) {
25484
25730
  invariant(request.intent_tx_hash.length > 0, "Empty intent_tx_hash");
25485
25731
  try {
25486
25732
  const response = await fetch(`${config.solverApiEndpoint}/status`, {
@@ -25501,13 +25747,16 @@ var SolverApiService = class {
25501
25747
  value: await response.json()
25502
25748
  };
25503
25749
  } catch (e) {
25504
- console.error(`[SolverApiService.getStatus] failed. Details: ${JSON.stringify(e)}`);
25750
+ logger.error(
25751
+ "[SolverApiService.getStatus] failed",
25752
+ e instanceof Error ? e : new Error(JSON.stringify(e, bigintReplacer))
25753
+ );
25505
25754
  return {
25506
25755
  ok: false,
25507
25756
  error: {
25508
25757
  detail: {
25509
25758
  code: exports.SolverIntentErrorCode.UNKNOWN,
25510
- message: e ? JSON.stringify(e) : "Unknown error"
25759
+ message: e ? JSON.stringify(e, bigintReplacer) : "Unknown error"
25511
25760
  }
25512
25761
  }
25513
25762
  };
@@ -25627,7 +25876,7 @@ var SwapService = class {
25627
25876
  * `fill_tx_hash` is populated only when `status === SolverIntentStatusCode.SOLVED (3)`.
25628
25877
  */
25629
25878
  async getStatus(request) {
25630
- return SolverApiService.getStatus(request, this.solver);
25879
+ return SolverApiService.getStatus(request, this.solver, this.config.logger);
25631
25880
  }
25632
25881
  /**
25633
25882
  * Notifies the solver API that an intent has been registered on the hub chain, triggering
@@ -25650,7 +25899,7 @@ var SwapService = class {
25650
25899
  */
25651
25900
  async postExecution(request) {
25652
25901
  try {
25653
- const result = await SolverApiService.postExecution(request, this.solver);
25902
+ const result = await SolverApiService.postExecution(request, this.solver, this.config.logger);
25654
25903
  if (result.ok) return result;
25655
25904
  const detail = result.error?.detail ?? {
25656
25905
  code: -999,
@@ -25923,7 +26172,7 @@ var SwapService = class {
25923
26172
  * - `raw: false` — broadcasts the transaction; `walletProvider` is required and must match `K`.
25924
26173
  *
25925
26174
  * Validates tokens and chain keys against the active `ConfigService` before constructing the
25926
- * intent. Bitcoin source chains require an additional RadFi access token step.
26175
+ * intent. Bitcoin source chains require an additional Bound Exchange access token step.
25927
26176
  *
25928
26177
  * @param _params - Intent parameters, source chain key, wallet provider (when `raw: false`),
25929
26178
  * and optional `skipSimulation` flag.
@@ -28404,11 +28653,13 @@ var MigrationService = class {
28404
28653
 
28405
28654
  // src/backendApi/BackendApiService.ts
28406
28655
  var BackendApiService = class {
28407
- constructor(config) {
28656
+ constructor(config, logger = consoleLogger) {
28408
28657
  this.config = config;
28409
28658
  this.headers = { ...config.headers };
28659
+ this.logger = logger;
28410
28660
  }
28411
28661
  headers;
28662
+ logger;
28412
28663
  /**
28413
28664
  * Execute a single HTTP request and return the parsed JSON body.
28414
28665
  *
@@ -28447,10 +28698,10 @@ var BackendApiService = class {
28447
28698
  if (error.name === "AbortError") {
28448
28699
  throw new Error("REQUEST_TIMEOUT", { cause: new Error(`Request timeout after ${timeout}ms`) });
28449
28700
  }
28450
- console.error("[BackendApiService] Request error:", error.message);
28701
+ this.logger.error("[BackendApiService] Request error", error);
28451
28702
  throw error;
28452
28703
  }
28453
- console.error("[BackendApiService] Unknown error:", error);
28704
+ this.logger.error("[BackendApiService] Unknown error", error);
28454
28705
  throw new Error("UNKNOWN_REQUEST_ERROR", { cause: error });
28455
28706
  }
28456
28707
  }
@@ -29120,7 +29371,7 @@ var BridgeService = class {
29120
29371
  * transaction simulation or batching). When `raw` is `false`, signs and submits the deposit
29121
29372
  * transaction via the provided wallet provider.
29122
29373
  *
29123
- * Bitcoin is only supported with `raw: false` because it requires the RadFi trading wallet
29374
+ * Bitcoin is only supported with `raw: false` because it requires the Bound Exchange trading wallet
29124
29375
  * derivation flow.
29125
29376
  *
29126
29377
  * @param _params - Bridge parameters including source/destination chain keys, token addresses,
@@ -29153,9 +29404,12 @@ var BridgeService = class {
29153
29404
  { ...baseCtx, field: "walletProvider" }
29154
29405
  );
29155
29406
  walletAddress = await this.spoke.bitcoin.getEffectiveWalletAddress(personalAddress);
29156
- await this.spoke.bitcoin.radfi.ensureRadfiAccessToken(_params.walletProvider);
29407
+ if (this.spoke.bitcoin.walletMode === "TRADING") {
29408
+ await this.spoke.bitcoin.radfi.ensureRadfiAccessToken(_params.walletProvider);
29409
+ }
29157
29410
  }
29158
- const hubWallet = await this.hubProvider.getUserHubWalletAddress(params.srcAddress, params.srcChainKey);
29411
+ const hubWallet = await this.hubProvider.getUserHubWalletAddress(walletAddress, params.srcChainKey);
29412
+ const effectiveSkipSimulation = skipSimulation || isBitcoinChainKeyType(params.srcChainKey) && this.spoke.bitcoin.walletMode === "USER";
29159
29413
  const data = this.buildBridgeData(params, srcToken, dstToken, this.config.bridge.partnerFee);
29160
29414
  const coreParams = {
29161
29415
  srcChainKey: params.srcChainKey,
@@ -29164,7 +29418,7 @@ var BridgeService = class {
29164
29418
  token: params.srcToken,
29165
29419
  amount: params.amount,
29166
29420
  data,
29167
- skipSimulation
29421
+ skipSimulation: effectiveSkipSimulation
29168
29422
  };
29169
29423
  const txResult = await this.spoke.deposit(
29170
29424
  _params.raw ? {
@@ -29177,7 +29431,7 @@ var BridgeService = class {
29177
29431
  }
29178
29432
  );
29179
29433
  if (!txResult.ok) {
29180
- console.error(txResult.error);
29434
+ this.config.logger.error("createBridgeIntent failed", txResult.error);
29181
29435
  if (isBridgeCreateIntentError(txResult.error)) return { ok: false, error: txResult.error };
29182
29436
  return {
29183
29437
  ok: false,
@@ -29192,7 +29446,7 @@ var BridgeService = class {
29192
29446
  }
29193
29447
  };
29194
29448
  } catch (error) {
29195
- console.error(error);
29449
+ this.config.logger.error("createBridgeIntent failed", error);
29196
29450
  if (isBridgeCreateIntentError(error)) return { ok: false, error };
29197
29451
  return {
29198
29452
  ok: false,
@@ -29362,7 +29616,7 @@ var BridgeService = class {
29362
29616
  value: availableDepositNormalised.isLessThan(assetManagerBalanceNormalised) ? { amount: availableDeposit, decimals: fromTokenInfo.decimals, type: "DEPOSIT_LIMIT" } : { amount: assetManagerBalance, decimals: toTokenInfo.decimals, type: "WITHDRAWAL_LIMIT" }
29363
29617
  };
29364
29618
  } catch (error) {
29365
- console.error(error);
29619
+ this.config.logger.error("getBridgeableAmount failed", error);
29366
29620
  if (isBridgeLookupError(error)) return { ok: false, error };
29367
29621
  return {
29368
29622
  ok: false,
@@ -29409,7 +29663,7 @@ var BridgeService = class {
29409
29663
  });
29410
29664
  return srcToken.vault.toLowerCase() === dstToken.vault.toLowerCase();
29411
29665
  } catch (error) {
29412
- console.error(error);
29666
+ this.config.logger.error("isBridgeable check failed", error);
29413
29667
  return false;
29414
29668
  }
29415
29669
  }
@@ -31799,7 +32053,7 @@ var ClService = class _ClService {
31799
32053
  };
31800
32054
  const txResult = await this.spoke.sendMessage(sendMessageParams);
31801
32055
  if (!txResult.ok) {
31802
- console.error("executeSupplyLiquidity error:", txResult.error);
32056
+ this.config.logger.error("executeSupplyLiquidity error", txResult.error);
31803
32057
  return {
31804
32058
  ok: false,
31805
32059
  error: txResult.error
@@ -31813,7 +32067,7 @@ var ClService = class _ClService {
31813
32067
  }
31814
32068
  };
31815
32069
  } catch (error) {
31816
- console.error("executeSupplyLiquidity error:", error);
32070
+ this.config.logger.error("executeSupplyLiquidity error", error);
31817
32071
  return {
31818
32072
  ok: false,
31819
32073
  error
@@ -32086,7 +32340,7 @@ var ClService = class _ClService {
32086
32340
  }
32087
32341
  return { ok: true, value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: hubTxHash } };
32088
32342
  } catch (error) {
32089
- console.error("supplyLiquidity error:", error);
32343
+ this.config.logger.error("supplyLiquidity error", error);
32090
32344
  return {
32091
32345
  ok: false,
32092
32346
  error
@@ -32230,7 +32484,7 @@ var ClService = class _ClService {
32230
32484
  }
32231
32485
  };
32232
32486
  } catch (error) {
32233
- console.error("getPoolRewardConfig error:", error);
32487
+ this.config.logger.error("getPoolRewardConfig error", error);
32234
32488
  return {
32235
32489
  ok: false,
32236
32490
  error: lookupFailed("dex", "getPoolRewardConfig", error)
@@ -32291,7 +32545,7 @@ var ClService = class _ClService {
32291
32545
  };
32292
32546
  const txResult = await this.spoke.sendMessage(sendMessageParams);
32293
32547
  if (!txResult.ok) {
32294
- console.error("executeClaimRewards error:", txResult.error);
32548
+ this.config.logger.error("executeClaimRewards error", txResult.error);
32295
32549
  return {
32296
32550
  ok: false,
32297
32551
  error: txResult.error
@@ -32305,7 +32559,7 @@ var ClService = class _ClService {
32305
32559
  }
32306
32560
  };
32307
32561
  } catch (error) {
32308
- console.error("executeClaimRewards error:", error);
32562
+ this.config.logger.error("executeClaimRewards error", error);
32309
32563
  return {
32310
32564
  ok: false,
32311
32565
  error
@@ -32347,7 +32601,7 @@ var ClService = class _ClService {
32347
32601
  }
32348
32602
  return { ok: true, value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: hubTxHash } };
32349
32603
  } catch (error) {
32350
- console.error("claimRewards error:", error);
32604
+ this.config.logger.error("claimRewards error", error);
32351
32605
  return {
32352
32606
  ok: false,
32353
32607
  error
@@ -32395,7 +32649,7 @@ var ClService = class _ClService {
32395
32649
  address: tokenAddress
32396
32650
  };
32397
32651
  } catch (error) {
32398
- console.error(`Failed to fetch token info for ${tokenAddress}:`, error);
32652
+ this.config.logger.error(`Failed to fetch token info for ${tokenAddress}`, error);
32399
32653
  return {
32400
32654
  symbol: "UNKNOWN",
32401
32655
  name: "Unknown Token",
@@ -32420,12 +32674,12 @@ var ClService = class _ClService {
32420
32674
  const oneShare = BigInt(10 ** 18);
32421
32675
  const result = await Erc4626Service.convertToAssets(statATokenAddress, oneShare, this.hubProvider.publicClient);
32422
32676
  if (!result.ok) {
32423
- console.error("[getStatATokenConversionRate] Failed to get conversion rate:", result.error);
32677
+ this.config.logger.error("[getStatATokenConversionRate] Failed to get conversion rate", result.error);
32424
32678
  return oneShare;
32425
32679
  }
32426
32680
  return result.value;
32427
32681
  } catch (error) {
32428
- console.error("[getStatATokenConversionRate] Error:", error);
32682
+ this.config.logger.error("[getStatATokenConversionRate] Error", error);
32429
32683
  return BigInt(10 ** 18);
32430
32684
  }
32431
32685
  }
@@ -32462,7 +32716,7 @@ var ClService = class _ClService {
32462
32716
  underlyingToken
32463
32717
  };
32464
32718
  } catch (error) {
32465
- console.error(`[getTokenEnrichmentData] Failed to enrich token ${token.address}:`, error);
32719
+ this.config.logger.error(`[getTokenEnrichmentData] Failed to enrich token ${token.address}`, error);
32466
32720
  return {
32467
32721
  token,
32468
32722
  isStatAToken: true
@@ -32532,7 +32786,7 @@ var ClService = class _ClService {
32532
32786
  rewardConfig = rewardConfigResult.value;
32533
32787
  }
32534
32788
  } catch (error) {
32535
- console.warn("Failed to fetch reward config for pool:", error);
32789
+ this.config.logger.warn("Failed to fetch reward config for pool", { error });
32536
32790
  }
32537
32791
  }
32538
32792
  return {
@@ -32568,7 +32822,7 @@ var ClService = class _ClService {
32568
32822
  }
32569
32823
  };
32570
32824
  } catch (error) {
32571
- console.error("Failed to fetch pool data:", error);
32825
+ this.config.logger.error("Failed to fetch pool data", error);
32572
32826
  return {
32573
32827
  ok: false,
32574
32828
  error: lookupFailed("dex", "getPoolData", error)
@@ -35352,14 +35606,25 @@ var MoneyMarketService = class _MoneyMarketService {
35352
35606
  );
35353
35607
  const dstChainKey = params.dstChainKey ?? srcChainKey;
35354
35608
  const dstAddress = params.dstAddress ?? params.srcAddress;
35355
- const [fromHubWallet, toHubWallet] = await Promise.all([
35356
- this.hubProvider.getUserHubWalletAddress(params.srcAddress, srcChainKey),
35357
- this.hubProvider.getUserHubWalletAddress(dstAddress, dstChainKey)
35358
- ]);
35609
+ const isBitcoinSrc = isBitcoinChainKeyType(srcChainKey);
35610
+ if (isBitcoinSrc && !_params.raw) {
35611
+ mmInvariant(
35612
+ walletProvider !== void 0 && isBitcoinWalletProviderType(walletProvider),
35613
+ `Invalid wallet provider for chain key: ${srcChainKey}. Expected bitcoin wallet provider.`,
35614
+ { ...baseCtx, field: "walletProvider" }
35615
+ );
35616
+ await this.spoke.bitcoin.radfi.ensureRadfiAccessToken(walletProvider);
35617
+ }
35618
+ const srcEffectiveAddress = isBitcoinSrc ? await this.spoke.bitcoin.getEffectiveWalletAddress(params.srcAddress) : params.srcAddress;
35619
+ const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(srcEffectiveAddress, srcChainKey);
35620
+ const toHubWallet = dstChainKey === srcChainKey && dstAddress === params.srcAddress ? fromHubWallet : await this.hubProvider.getUserHubWalletAddress(
35621
+ isBitcoinChainKeyType(dstChainKey) ? await this.spoke.bitcoin.getEffectiveWalletAddress(dstAddress) : dstAddress,
35622
+ dstChainKey
35623
+ );
35359
35624
  const data = this.buildSupplyData(srcChainKey, params.token, params.amount, toHubWallet);
35360
35625
  const coreParams = {
35361
35626
  srcChainKey,
35362
- srcAddress: params.srcAddress,
35627
+ srcAddress: srcEffectiveAddress,
35363
35628
  to: fromHubWallet,
35364
35629
  token: params.token,
35365
35630
  amount: params.amount,
@@ -35399,6 +35664,17 @@ var MoneyMarketService = class _MoneyMarketService {
35399
35664
  }
35400
35665
  }
35401
35666
  // ==== borrow ==========================================================================
35667
+ /**
35668
+ * Build the relay submit/poll identity for a money-market borrow/withdraw.
35669
+ *
35670
+ * Bitcoin borrow/withdraw are on-demand: the spoke result is a signed payload JSON that the relay
35671
+ * submits under the literal "withdraw" tx_hash and tracks under a derived `od:<hash>` poll id
35672
+ * (see {@link BitcoinSpokeService.getOnDemandRelayIdentity}). Every other chain relays and polls by
35673
+ * its real spoke tx hash, so `pollTxHash` is undefined and `srcChainTxHash` stays the spoke tx.
35674
+ */
35675
+ buildRelayIdentity(srcChainKey, tx, relayData) {
35676
+ return isBitcoinChainKeyType(srcChainKey) ? this.spoke.bitcoin.getOnDemandRelayIdentity(tx) : { srcTxHash: tx, data: relayData, pollTxHash: void 0 };
35677
+ }
35402
35678
  /**
35403
35679
  * Borrow tokens from the money market lending pool and wait for the cross-chain relay to
35404
35680
  * deliver the funds to the destination address.
@@ -35434,9 +35710,9 @@ var MoneyMarketService = class _MoneyMarketService {
35434
35710
  value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: txResult.value.tx }
35435
35711
  };
35436
35712
  }
35713
+ const relayIdentity = this.buildRelayIdentity(srcChainKey, txResult.value.tx, txResult.value.relayData);
35437
35714
  const packet = await relayTxAndWaitPacket({
35438
- srcTxHash: txResult.value.tx,
35439
- data: txResult.value.relayData,
35715
+ ...relayIdentity,
35440
35716
  chainKey: srcChainKey,
35441
35717
  relayerApiEndpoint: this.relayerApiEndpoint,
35442
35718
  timeout
@@ -35451,7 +35727,13 @@ var MoneyMarketService = class _MoneyMarketService {
35451
35727
  dstChainKey: baseCtx.dstChainKey
35452
35728
  })
35453
35729
  };
35454
- return { ok: true, value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: packet.value.dst_tx_hash } };
35730
+ return {
35731
+ ok: true,
35732
+ value: {
35733
+ srcChainTxHash: relayIdentity.pollTxHash ?? txResult.value.tx,
35734
+ dstChainTxHash: packet.value.dst_tx_hash
35735
+ }
35736
+ };
35455
35737
  } catch (error) {
35456
35738
  if (isMoneyMarketOrchestrationError(error)) return { ok: false, error };
35457
35739
  return {
@@ -35493,7 +35775,8 @@ var MoneyMarketService = class _MoneyMarketService {
35493
35775
  field: "token"
35494
35776
  });
35495
35777
  const encodedDstAddress = encodeAddress(dstChainKey, dstAddress);
35496
- const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(params.srcAddress, srcChainKey);
35778
+ const srcEffectiveAddress = isBitcoinChainKeyType(srcChainKey) ? await this.spoke.bitcoin.getEffectiveWalletAddress(params.srcAddress) : params.srcAddress;
35779
+ const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(srcEffectiveAddress, srcChainKey);
35497
35780
  const payload = this.buildBorrowData(
35498
35781
  fromHubWallet,
35499
35782
  encodedDstAddress,
@@ -35572,144 +35855,151 @@ var MoneyMarketService = class _MoneyMarketService {
35572
35855
  }
35573
35856
  const needsRelay = srcChainKey !== hubChainId || params.dstChainKey != null && params.dstAddress != null && params.dstChainKey !== hubChainId && params.dstAddress !== walletRouter;
35574
35857
  if (!needsRelay) {
35575
- return {
35576
- ok: true,
35577
- value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: txResult.value.tx }
35578
- };
35579
- }
35580
- const packet = await relayTxAndWaitPacket({
35581
- srcTxHash: txResult.value.tx,
35582
- data: txResult.value.relayData,
35583
- chainKey: srcChainKey,
35584
- relayerApiEndpoint: this.relayerApiEndpoint,
35585
- timeout
35586
- });
35587
- if (!packet.ok)
35588
- return {
35589
- ok: false,
35590
- error: mapRelayFailure(packet.error, {
35591
- feature: "moneyMarket",
35592
- action: baseCtx.action,
35593
- srcChainKey: baseCtx.srcChainKey,
35594
- dstChainKey: baseCtx.dstChainKey
35595
- })
35596
- };
35597
- return { ok: true, value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: packet.value.dst_tx_hash } };
35598
- } catch (error) {
35599
- if (isMoneyMarketOrchestrationError(error)) return { ok: false, error };
35600
- return {
35601
- ok: false,
35602
- error: executionFailed("moneyMarket", error, { ...baseCtx, phase: "intentCreation" })
35603
- };
35604
- }
35605
- }
35606
- /**
35607
- * Build and optionally broadcast the spoke-side withdraw message without waiting for the
35608
- * cross-chain relay to deliver funds.
35609
- *
35610
- * Use this when you need manual relay control or want to sign and broadcast the transaction
35611
- * yourself. Pass `{ raw: true }` to receive unsigned calldata instead of executing.
35612
- *
35613
- * @param _params - Withdraw action params plus `raw` flag and optional `skipSimulation`.
35614
- * @returns The spoke transaction result (hash or raw calldata) plus `relayData` required to
35615
- * trigger the hub-side withdrawal via {@link relayTxAndWaitPacket}.
35616
- */
35617
- async createWithdrawIntent(_params) {
35618
- const { params, walletProvider } = _params;
35619
- const srcChainKey = params.srcChainKey;
35620
- const skipSimulation = _params.skipSimulation ?? false;
35621
- const baseCtx = { srcChainKey, dstChainKey: params.dstChainKey, action: "withdraw" };
35622
- try {
35623
- mmInvariant(params.action === "withdraw", "Invalid action", { ...baseCtx, field: "action" });
35624
- mmInvariant(params.token.length > 0, "Token is required", { ...baseCtx, field: "token" });
35625
- mmInvariant(params.amount > 0n, "Amount must be greater than 0", { ...baseCtx, field: "amount" });
35626
- mmInvariant(
35627
- isUndefinedOrValidWalletProviderForChainKey(srcChainKey, walletProvider),
35628
- `Invalid wallet provider for chain key: ${srcChainKey}, walletProvider.chainType: ${walletProvider?.chainType}`,
35629
- { ...baseCtx, field: "walletProvider" }
35630
- );
35631
- const dstChainKey = params.dstChainKey ?? srcChainKey;
35632
- const dstAddress = params.dstAddress ?? params.srcAddress;
35633
- mmInvariant(
35634
- this.config.isMoneyMarketSupportedToken(dstChainKey, params.token),
35635
- `Unsupported spoke chain (${dstChainKey}) token: ${params.token}`,
35636
- { ...baseCtx, field: "token" }
35637
- );
35638
- const encodedDstAddress = encodeAddress(dstChainKey, dstAddress);
35639
- const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(params.srcAddress, srcChainKey);
35640
- const payload = this.buildWithdrawData(
35641
- fromHubWallet,
35642
- encodedDstAddress,
35643
- params.token,
35644
- params.amount,
35645
- dstChainKey
35646
- );
35647
- const coreParams = {
35648
- srcChainKey,
35649
- srcAddress: params.srcAddress,
35650
- dstChainKey: HUB_CHAIN_KEY,
35651
- dstAddress: fromHubWallet,
35652
- payload,
35653
- skipSimulation
35654
- };
35655
- const sendMessageParams = _params.raw ? {
35656
- ...coreParams,
35657
- raw: true
35658
- } : {
35659
- ...coreParams,
35660
- raw: false,
35661
- walletProvider: _params.walletProvider
35662
- };
35663
- const txResult = await this.spoke.sendMessage(sendMessageParams);
35664
- if (!txResult.ok) {
35665
- if (isMoneyMarketCreateIntentError(txResult.error)) return { ok: false, error: txResult.error };
35666
- return {
35667
- ok: false,
35668
- error: intentCreationFailed("moneyMarket", txResult.error, baseCtx)
35669
- };
35670
- }
35671
- return {
35672
- ok: true,
35673
- value: {
35674
- tx: txResult.value,
35675
- relayData: { address: fromHubWallet, payload }
35676
- }
35677
- };
35678
- } catch (error) {
35679
- if (isMoneyMarketCreateIntentError(error)) return { ok: false, error };
35680
- return {
35681
- ok: false,
35682
- error: intentCreationFailed("moneyMarket", error, baseCtx)
35683
- };
35684
- }
35685
- }
35686
- // ==== repay ===========================================================================
35687
- /**
35688
- * Repay a borrowed position in the money market lending pool and wait for the cross-chain
35689
- * relay to settle on the hub.
35690
- *
35691
- * Hub-chain callers skip the relay step. The repayment is credited to `dstAddress` on
35692
- * `dstChainKey` (defaulting to the source address and chain when omitted).
35693
- *
35694
- * @param _params - Repay action params: `srcChainKey`, `srcAddress`, `token`, `amount`,
35695
- * `walletProvider`, and optional `dstChainKey`/`dstAddress`.
35696
- * @returns A pair of transaction hashes — `srcChainTxHash` (spoke) and `dstChainTxHash` (hub).
35697
- */
35698
- async repay(_params) {
35699
- const { params, timeout = DEFAULT_RELAY_TX_TIMEOUT } = _params;
35700
- const srcChainKey = params.srcChainKey;
35701
- const baseCtx = { srcChainKey, dstChainKey: params.dstChainKey, action: "repay" };
35702
- try {
35703
- const txResult = await this.createRepayIntent(_params);
35704
- if (!txResult.ok) return { ok: false, error: txResult.error };
35705
- const verify = await this.spoke.verifyTxHash({ txHash: txResult.value.tx, chainKey: srcChainKey });
35706
- if (!verify.ok) {
35707
- return {
35708
- ok: false,
35709
- error: verifyFailed("moneyMarket", verify.error, baseCtx)
35710
- };
35711
- }
35712
- if (isHubChainKeyType(srcChainKey)) {
35858
+ return {
35859
+ ok: true,
35860
+ value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: txResult.value.tx }
35861
+ };
35862
+ }
35863
+ const relayIdentity = this.buildRelayIdentity(srcChainKey, txResult.value.tx, txResult.value.relayData);
35864
+ const packet = await relayTxAndWaitPacket({
35865
+ ...relayIdentity,
35866
+ chainKey: srcChainKey,
35867
+ relayerApiEndpoint: this.relayerApiEndpoint,
35868
+ timeout
35869
+ });
35870
+ if (!packet.ok)
35871
+ return {
35872
+ ok: false,
35873
+ error: mapRelayFailure(packet.error, {
35874
+ feature: "moneyMarket",
35875
+ action: baseCtx.action,
35876
+ srcChainKey: baseCtx.srcChainKey,
35877
+ dstChainKey: baseCtx.dstChainKey
35878
+ })
35879
+ };
35880
+ return {
35881
+ ok: true,
35882
+ value: {
35883
+ srcChainTxHash: relayIdentity.pollTxHash ?? txResult.value.tx,
35884
+ dstChainTxHash: packet.value.dst_tx_hash
35885
+ }
35886
+ };
35887
+ } catch (error) {
35888
+ if (isMoneyMarketOrchestrationError(error)) return { ok: false, error };
35889
+ return {
35890
+ ok: false,
35891
+ error: executionFailed("moneyMarket", error, { ...baseCtx, phase: "intentCreation" })
35892
+ };
35893
+ }
35894
+ }
35895
+ /**
35896
+ * Build and optionally broadcast the spoke-side withdraw message without waiting for the
35897
+ * cross-chain relay to deliver funds.
35898
+ *
35899
+ * Use this when you need manual relay control or want to sign and broadcast the transaction
35900
+ * yourself. Pass `{ raw: true }` to receive unsigned calldata instead of executing.
35901
+ *
35902
+ * @param _params - Withdraw action params plus `raw` flag and optional `skipSimulation`.
35903
+ * @returns The spoke transaction result (hash or raw calldata) plus `relayData` required to
35904
+ * trigger the hub-side withdrawal via {@link relayTxAndWaitPacket}.
35905
+ */
35906
+ async createWithdrawIntent(_params) {
35907
+ const { params, walletProvider } = _params;
35908
+ const srcChainKey = params.srcChainKey;
35909
+ const skipSimulation = _params.skipSimulation ?? false;
35910
+ const baseCtx = { srcChainKey, dstChainKey: params.dstChainKey, action: "withdraw" };
35911
+ try {
35912
+ mmInvariant(params.action === "withdraw", "Invalid action", { ...baseCtx, field: "action" });
35913
+ mmInvariant(params.token.length > 0, "Token is required", { ...baseCtx, field: "token" });
35914
+ mmInvariant(params.amount > 0n, "Amount must be greater than 0", { ...baseCtx, field: "amount" });
35915
+ mmInvariant(
35916
+ isUndefinedOrValidWalletProviderForChainKey(srcChainKey, walletProvider),
35917
+ `Invalid wallet provider for chain key: ${srcChainKey}, walletProvider.chainType: ${walletProvider?.chainType}`,
35918
+ { ...baseCtx, field: "walletProvider" }
35919
+ );
35920
+ const dstChainKey = params.dstChainKey ?? srcChainKey;
35921
+ const dstAddress = params.dstAddress ?? params.srcAddress;
35922
+ mmInvariant(
35923
+ this.config.isMoneyMarketSupportedToken(dstChainKey, params.token),
35924
+ `Unsupported spoke chain (${dstChainKey}) token: ${params.token}`,
35925
+ { ...baseCtx, field: "token" }
35926
+ );
35927
+ const encodedDstAddress = encodeAddress(dstChainKey, dstAddress);
35928
+ const srcEffectiveAddress = isBitcoinChainKeyType(srcChainKey) ? await this.spoke.bitcoin.getEffectiveWalletAddress(params.srcAddress) : params.srcAddress;
35929
+ const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(srcEffectiveAddress, srcChainKey);
35930
+ const payload = this.buildWithdrawData(
35931
+ fromHubWallet,
35932
+ encodedDstAddress,
35933
+ params.token,
35934
+ params.amount,
35935
+ dstChainKey
35936
+ );
35937
+ const coreParams = {
35938
+ srcChainKey,
35939
+ srcAddress: params.srcAddress,
35940
+ dstChainKey: HUB_CHAIN_KEY,
35941
+ dstAddress: fromHubWallet,
35942
+ payload,
35943
+ skipSimulation
35944
+ };
35945
+ const sendMessageParams = _params.raw ? {
35946
+ ...coreParams,
35947
+ raw: true
35948
+ } : {
35949
+ ...coreParams,
35950
+ raw: false,
35951
+ walletProvider: _params.walletProvider
35952
+ };
35953
+ const txResult = await this.spoke.sendMessage(sendMessageParams);
35954
+ if (!txResult.ok) {
35955
+ if (isMoneyMarketCreateIntentError(txResult.error)) return { ok: false, error: txResult.error };
35956
+ return {
35957
+ ok: false,
35958
+ error: intentCreationFailed("moneyMarket", txResult.error, baseCtx)
35959
+ };
35960
+ }
35961
+ return {
35962
+ ok: true,
35963
+ value: {
35964
+ tx: txResult.value,
35965
+ relayData: { address: fromHubWallet, payload }
35966
+ }
35967
+ };
35968
+ } catch (error) {
35969
+ if (isMoneyMarketCreateIntentError(error)) return { ok: false, error };
35970
+ return {
35971
+ ok: false,
35972
+ error: intentCreationFailed("moneyMarket", error, baseCtx)
35973
+ };
35974
+ }
35975
+ }
35976
+ // ==== repay ===========================================================================
35977
+ /**
35978
+ * Repay a borrowed position in the money market lending pool and wait for the cross-chain
35979
+ * relay to settle on the hub.
35980
+ *
35981
+ * Hub-chain callers skip the relay step. The repayment is credited to `dstAddress` on
35982
+ * `dstChainKey` (defaulting to the source address and chain when omitted).
35983
+ *
35984
+ * @param _params - Repay action params: `srcChainKey`, `srcAddress`, `token`, `amount`,
35985
+ * `walletProvider`, and optional `dstChainKey`/`dstAddress`.
35986
+ * @returns A pair of transaction hashes — `srcChainTxHash` (spoke) and `dstChainTxHash` (hub).
35987
+ */
35988
+ async repay(_params) {
35989
+ const { params, timeout = DEFAULT_RELAY_TX_TIMEOUT } = _params;
35990
+ const srcChainKey = params.srcChainKey;
35991
+ const baseCtx = { srcChainKey, dstChainKey: params.dstChainKey, action: "repay" };
35992
+ try {
35993
+ const txResult = await this.createRepayIntent(_params);
35994
+ if (!txResult.ok) return { ok: false, error: txResult.error };
35995
+ const verify = await this.spoke.verifyTxHash({ txHash: txResult.value.tx, chainKey: srcChainKey });
35996
+ if (!verify.ok) {
35997
+ return {
35998
+ ok: false,
35999
+ error: verifyFailed("moneyMarket", verify.error, baseCtx)
36000
+ };
36001
+ }
36002
+ if (isHubChainKeyType(srcChainKey)) {
35713
36003
  return {
35714
36004
  ok: true,
35715
36005
  value: { srcChainTxHash: txResult.value.tx, dstChainTxHash: txResult.value.tx }
@@ -35773,14 +36063,25 @@ var MoneyMarketService = class _MoneyMarketService {
35773
36063
  );
35774
36064
  const dstChainKey = params.dstChainKey ?? srcChainKey;
35775
36065
  const dstAddress = params.dstAddress ?? params.srcAddress;
35776
- const [fromHubWallet, toHubWallet] = await Promise.all([
35777
- this.hubProvider.getUserHubWalletAddress(params.srcAddress, srcChainKey),
35778
- this.hubProvider.getUserHubWalletAddress(dstAddress, dstChainKey)
35779
- ]);
36066
+ const isBitcoinSrc = isBitcoinChainKeyType(srcChainKey);
36067
+ if (isBitcoinSrc && !_params.raw) {
36068
+ mmInvariant(
36069
+ walletProvider !== void 0 && isBitcoinWalletProviderType(walletProvider),
36070
+ `Invalid wallet provider for chain key: ${srcChainKey}. Expected bitcoin wallet provider.`,
36071
+ { ...baseCtx, field: "walletProvider" }
36072
+ );
36073
+ await this.spoke.bitcoin.radfi.ensureRadfiAccessToken(walletProvider);
36074
+ }
36075
+ const srcEffectiveAddress = isBitcoinSrc ? await this.spoke.bitcoin.getEffectiveWalletAddress(params.srcAddress) : params.srcAddress;
36076
+ const fromHubWallet = await this.hubProvider.getUserHubWalletAddress(srcEffectiveAddress, srcChainKey);
36077
+ const toHubWallet = dstChainKey === srcChainKey && dstAddress === params.srcAddress ? fromHubWallet : await this.hubProvider.getUserHubWalletAddress(
36078
+ isBitcoinChainKeyType(dstChainKey) ? await this.spoke.bitcoin.getEffectiveWalletAddress(dstAddress) : dstAddress,
36079
+ dstChainKey
36080
+ );
35780
36081
  const data = this.buildRepayData(srcChainKey, params.token, params.amount, toHubWallet);
35781
36082
  const coreParams = {
35782
36083
  srcChainKey,
35783
- srcAddress: params.srcAddress,
36084
+ srcAddress: srcEffectiveAddress,
35784
36085
  to: fromHubWallet,
35785
36086
  token: params.token,
35786
36087
  amount: params.amount,
@@ -36341,9 +36642,9 @@ var PartnerFeeClaimService = class {
36341
36642
  if (typeof balanceResult === "bigint") {
36342
36643
  balance = balanceResult;
36343
36644
  } else {
36344
- console.warn(
36345
- `[PartnerFeeClaimService] Unexpected balance result format for ${entry.hubAsset.symbol} (${entry.assetAddress}):`,
36346
- balanceResult
36645
+ this.config.logger.warn(
36646
+ `[PartnerFeeClaimService] Unexpected balance result format for ${entry.hubAsset.symbol} (${entry.assetAddress})`,
36647
+ { balanceResult }
36347
36648
  );
36348
36649
  balance = 0n;
36349
36650
  }
@@ -36679,7 +36980,8 @@ var PartnerFeeClaimService = class {
36679
36980
  }
36680
36981
  const solverExecutionResponse = await SolverApiService.postExecution(
36681
36982
  { intent_tx_hash: intentTxHash },
36682
- this.config.solver
36983
+ this.config.solver,
36984
+ this.config.logger
36683
36985
  );
36684
36986
  if (!solverExecutionResponse.ok) {
36685
36987
  return solverExecutionResponse;
@@ -36760,7 +37062,12 @@ var RecoveryService = class {
36760
37062
  srcChainKey: chainKey,
36761
37063
  field: "chainKey"
36762
37064
  });
36763
- 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
+ );
36764
37071
  if (entries.length === 0) {
36765
37072
  return { ok: true, value: [] };
36766
37073
  }
@@ -36862,6 +37169,871 @@ var RecoveryService = class {
36862
37169
  }
36863
37170
  };
36864
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
+
36865
38037
  // src/shared/entities/Sodax.ts
36866
38038
  var Sodax = class {
36867
38039
  instanceConfig;
@@ -36883,6 +38055,8 @@ var Sodax = class {
36883
38055
  // Recovery service for withdrawing stuck hub-wallet assets back to a spoke chain
36884
38056
  dex;
36885
38057
  // Dex service enabling DEX operations
38058
+ leverageYield;
38059
+ // Leverage-yield service: cross-chain deposits / withdrawals into ERC-4626 leverage vaults on Sonic
36886
38060
  config;
36887
38061
  // Config service enabling configuration data fetching from the backend API or fallbacking to default values
36888
38062
  hubProvider;
@@ -36890,9 +38064,10 @@ var Sodax = class {
36890
38064
  spoke;
36891
38065
  // spoke service enabling spoke chain operations
36892
38066
  constructor(config) {
38067
+ const logger = resolveLogger(config?.logger);
36893
38068
  this.instanceConfig = config ? mergeSodaxConfig(sodaxConfig, config) : sodaxConfig;
36894
- this.backendApi = new BackendApiService(this.instanceConfig.api);
36895
- this.config = new ConfigService({ api: this.backendApi, config: this.instanceConfig, userConfig: config });
38069
+ this.backendApi = new BackendApiService(this.instanceConfig.api, logger);
38070
+ this.config = new ConfigService({ api: this.backendApi, config: this.instanceConfig, userConfig: config, logger });
36896
38071
  this.hubProvider = new EvmHubProvider({ config: this.config });
36897
38072
  this.spoke = new SpokeService({ config: this.config, hubProvider: this.hubProvider });
36898
38073
  this.swaps = new SwapService({
@@ -36927,6 +38102,11 @@ var Sodax = class {
36927
38102
  config: this.config,
36928
38103
  spoke: this.spoke
36929
38104
  });
38105
+ this.leverageYield = new LeverageYieldService({
38106
+ hubProvider: this.hubProvider,
38107
+ config: this.config,
38108
+ spoke: this.spoke
38109
+ });
36930
38110
  }
36931
38111
  /**
36932
38112
  * Initializes the Sodax instance with dynamic configuration.
@@ -37035,8 +38215,11 @@ exports.IntentsAbi = IntentsAbi;
37035
38215
  exports.LOOKUP_CODES = LOOKUP_CODES;
37036
38216
  exports.LTV_PRECISION = LTV_PRECISION;
37037
38217
  exports.LendingPoolService = LendingPoolService;
38218
+ exports.LeverageYieldService = LeverageYieldService;
37038
38219
  exports.LockupMultiplier = LockupMultiplier;
37039
38220
  exports.LockupPeriod = LockupPeriod;
38221
+ exports.LsodaSymbols = LsodaSymbols;
38222
+ exports.LsodaTokens = LsodaTokens;
37040
38223
  exports.MAX_UINT256 = MAX_UINT256;
37041
38224
  exports.MigrationService = MigrationService;
37042
38225
  exports.MintPositionEventAbi = MintPositionEventAbi;
@@ -37045,6 +38228,7 @@ exports.MoneyMarketService = MoneyMarketService;
37045
38228
  exports.NEAR_CHAIN_KEYS = NEAR_CHAIN_KEYS;
37046
38229
  exports.NEAR_CHAIN_KEYS_SET = NEAR_CHAIN_KEYS_SET;
37047
38230
  exports.NEAR_DEFAULT_GAS = NEAR_DEFAULT_GAS;
38231
+ exports.NEAR_STORAGE_DEPOSIT = NEAR_STORAGE_DEPOSIT;
37048
38232
  exports.NearSpokeService = NearSpokeService;
37049
38233
  exports.PartnerFeeClaimService = PartnerFeeClaimService;
37050
38234
  exports.PartnerService = PartnerService;
@@ -37108,6 +38292,7 @@ exports.bnUSDLegacyTokens = bnUSDLegacyTokens;
37108
38292
  exports.bridgeConfig = bridgeConfig;
37109
38293
  exports.bridgeInvariant = bridgeInvariant;
37110
38294
  exports.bscSupportedTokens = bscSupportedTokens;
38295
+ exports.calcOpReturnOutputVbytes = calcOpReturnOutputVbytes;
37111
38296
  exports.calculateAllReserveIncentives = calculateAllReserveIncentives;
37112
38297
  exports.calculateAllUserIncentives = calculateAllUserIncentives;
37113
38298
  exports.calculateAvailableBorrowsMarketReferenceCurrency = calculateAvailableBorrowsMarketReferenceCurrency;
@@ -37126,6 +38311,7 @@ exports.clRouterAbi = clRouterAbi;
37126
38311
  exports.clTickLensAbi = clTickLensAbi;
37127
38312
  exports.concentratedLiquidityConfig = concentratedLiquidityConfig;
37128
38313
  exports.connectionAbi = connectionAbi;
38314
+ exports.consoleLogger = consoleLogger;
37129
38315
  exports.convertTransactionInstructionToRaw = convertTransactionInstructionToRaw;
37130
38316
  exports.createInvariant = createInvariant;
37131
38317
  exports.deepMerge = deepMerge;
@@ -37218,6 +38404,13 @@ exports.isInjectiveChainKeyType = isInjectiveChainKeyType;
37218
38404
  exports.isJsonRpcPayloadResponse = isJsonRpcPayloadResponse;
37219
38405
  exports.isLegacybnUSDChainId = isLegacybnUSDChainId;
37220
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;
37221
38414
  exports.isMigrateOrchestrationError = isMigrateOrchestrationError;
37222
38415
  exports.isMigrationAllowanceCheckError = isMigrationAllowanceCheckError;
37223
38416
  exports.isMigrationApproveError = isMigrationApproveError;
@@ -37284,6 +38477,9 @@ exports.isUndefinedOrValidWalletProviderForChainKey = isUndefinedOrValidWalletPr
37284
38477
  exports.isUnifiedBnUSDMigrateParams = isUnifiedBnUSDMigrateParams;
37285
38478
  exports.isValidWalletProviderForChainKey = isValidWalletProviderForChainKey;
37286
38479
  exports.kaiaSupportedTokens = kaiaSupportedTokens;
38480
+ exports.leverageYieldConfig = leverageYieldConfig;
38481
+ exports.leverageYieldInvariant = leverageYieldInvariant;
38482
+ exports.leverageYieldVaults = leverageYieldVaults;
37287
38483
  exports.lightlinkSupportedTokens = lightlinkSupportedTokens;
37288
38484
  exports.lookupFailed = lookupFailed;
37289
38485
  exports.mapRelayFailure = mapRelayFailure;
@@ -37302,6 +38498,7 @@ exports.newbnUSDSpokeChainIds = newbnUSDSpokeChainIds;
37302
38498
  exports.normalize = normalize;
37303
38499
  exports.normalizeBN = normalizeBN;
37304
38500
  exports.normalizePsbtToBase64 = normalizePsbtToBase64;
38501
+ exports.normalizeSignatureToBase64 = normalizeSignatureToBase64;
37305
38502
  exports.normalizedToUsd = normalizedToUsd;
37306
38503
  exports.optimismSupportedTokens = optimismSupportedTokens;
37307
38504
  exports.pancakeSwapInfinityDefaultHookAbi = pancakeSwapInfinityDefaultHookAbi;
@@ -37326,9 +38523,11 @@ exports.relayTxAndWaitPacket = relayTxAndWaitPacket;
37326
38523
  exports.requestAddress = requestAddress;
37327
38524
  exports.requestJsonRpc = requestJsonRpc;
37328
38525
  exports.requestSigning = requestSigning;
38526
+ exports.resolveLogger = resolveLogger;
37329
38527
  exports.retry = retry;
37330
38528
  exports.reverseEncodeAddress = reverseEncodeAddress;
37331
38529
  exports.serializeAddressData = serializeAddressData;
38530
+ exports.silentLogger = silentLogger;
37332
38531
  exports.sleep = sleep;
37333
38532
  exports.sodaxConfig = sodaxConfig;
37334
38533
  exports.sodaxInvariant = sodaxInvariant;
@@ -37356,6 +38555,7 @@ exports.swapsConfig = swapsConfig;
37356
38555
  exports.uiPoolDataAbi = uiPoolDataAbi;
37357
38556
  exports.universalRouterAbi = universalRouterAbi;
37358
38557
  exports.unknownFailed = unknownFailed;
38558
+ exports.usesBip322MessageSigning = usesBip322MessageSigning;
37359
38559
  exports.valueToBigNumber = valueToBigNumber;
37360
38560
  exports.valueToZDBigNumber = valueToZDBigNumber;
37361
38561
  exports.variableDebtTokenAbi = variableDebtTokenAbi;