@liquidium/client 0.1.2 → 0.2.1

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.js CHANGED
@@ -1,9 +1,10 @@
1
- import { encodeFunctionData, isAddress, getAddress, createPublicClient, http } from 'viem';
1
+ import { encodeFunctionData, createPublicClient, http, isAddress, getAddress } from 'viem';
2
2
  import { mainnet } from 'viem/chains';
3
3
  import { Actor, HttpAgent } from '@icp-sdk/core/agent';
4
4
  import { Principal } from '@icp-sdk/core/principal';
5
5
  import { encodeIcrcAccount } from '@dfinity/ledger-icrc';
6
6
  import { Principal as Principal$1 } from '@dfinity/principal';
7
+ import { validate, Network } from 'bitcoin-address-validation';
7
8
 
8
9
  // src/client.ts
9
10
 
@@ -57,7 +58,8 @@ function resolveCanisterIds(environment = DEFAULT_ENVIRONMENT, overrides) {
57
58
  }
58
59
  var DEFAULT_TIMEOUT_MS = 3e4;
59
60
  var CK_CANISTER_IDS = {
60
- btcMinter: "mqygn-kiaaa-aaaar-qaadq-cai"};
61
+ btcMinter: "mqygn-kiaaa-aaaar-qaadq-cai",
62
+ btcLedger: "mxzaz-hqaaa-aaaar-qaada-cai"};
61
63
 
62
64
  // src/core/errors.ts
63
65
  var LiquidiumErrorCode = {
@@ -1593,6 +1595,11 @@ function buildInstantLoanAddressLookupPath(request) {
1593
1595
  const query = new URLSearchParams({ address: request.address });
1594
1596
  return `${INSTANT_LOANS}/address?${query.toString()}`;
1595
1597
  }
1598
+ function buildInstantLoanCollateralHintPath(request) {
1599
+ return `${INSTANT_LOANS}/${encodeURIComponent(
1600
+ request.loanId.toString()
1601
+ )}/collateral-hint`;
1602
+ }
1596
1603
  var SdkApiPath = {
1597
1604
  inflow: INFLOW,
1598
1605
  instantLoans: INSTANT_LOANS
@@ -2215,6 +2222,24 @@ function mapHistoryStatusToApi(status) {
2215
2222
  return status.toUpperCase();
2216
2223
  }
2217
2224
 
2225
+ // src/core/utils/asset-decimals.ts
2226
+ var ASSET_NATIVE_DECIMALS = {
2227
+ BTC: 8n,
2228
+ USDC: 6n,
2229
+ USDT: 6n,
2230
+ SOL: 9n
2231
+ };
2232
+ function getAssetNativeDecimals(asset) {
2233
+ const decimals = ASSET_NATIVE_DECIMALS[asset];
2234
+ if (decimals === void 0) {
2235
+ throw new LiquidiumError(
2236
+ LiquidiumErrorCode.VALIDATION_ERROR,
2237
+ `Native decimals are not configured for asset: ${asset}`
2238
+ );
2239
+ }
2240
+ return decimals;
2241
+ }
2242
+
2218
2243
  // src/generated/canisters/ck-btc-minter/declaration.js
2219
2244
  var idlFactory3 = ({ IDL }) => {
2220
2245
  const Mode = IDL.Variant({
@@ -3080,6 +3105,39 @@ function assertSupportsIcrcAccountInflowTarget(asset) {
3080
3105
  );
3081
3106
  }
3082
3107
 
3108
+ // src/core/borrow-minimums.ts
3109
+ var MIN_BORROW_AMOUNTS_BY_ASSET = {
3110
+ BTC: 5100n,
3111
+ // 5,100 sats = 0.000051 BTC
3112
+ USDC: 1000000n,
3113
+ // 1 USDC
3114
+ USDT: 1000000n
3115
+ // 1 USDT
3116
+ };
3117
+ function getMinimumBorrowAmount(asset) {
3118
+ if (!isMinimumBorrowAsset(asset)) {
3119
+ return 0n;
3120
+ }
3121
+ return MIN_BORROW_AMOUNTS_BY_ASSET[asset];
3122
+ }
3123
+ function getBorrowAmountMinimumValidationError(params) {
3124
+ const minimumAmount = getMinimumBorrowAmount(params.asset);
3125
+ if (minimumAmount <= 0n || params.amount >= minimumAmount) {
3126
+ return null;
3127
+ }
3128
+ return {
3129
+ asset: params.asset,
3130
+ minimumAmount,
3131
+ message: formatMinimumBorrowAmountMessage(params.asset, minimumAmount)
3132
+ };
3133
+ }
3134
+ function formatMinimumBorrowAmountMessage(asset, minimumAmount) {
3135
+ return `Borrow amount must be at least ${minimumAmount} base units for ${asset}`;
3136
+ }
3137
+ function isMinimumBorrowAsset(asset) {
3138
+ return Object.hasOwn(MIN_BORROW_AMOUNTS_BY_ASSET, asset);
3139
+ }
3140
+
3083
3141
  // src/modules/quote/types.ts
3084
3142
  var QuoteValidationErrorCode = /* @__PURE__ */ ((QuoteValidationErrorCode2) => {
3085
3143
  QuoteValidationErrorCode2["INVALID_LTV"] = "INVALID_LTV";
@@ -3102,7 +3160,6 @@ var BASIS_POINTS_DENOMINATOR = 10000n;
3102
3160
  var BPS_PER_PERCENT = 100n;
3103
3161
  var MIN_LTV_BPS = 0n;
3104
3162
  var HIGH_LTV_WARNING_THRESHOLD_BPS = 8000n;
3105
- var MIN_BORROW_AMOUNT_BASE_UNITS = 5000n;
3106
3163
  var INTERNAL_USD_DECIMAL_PLACES = 8;
3107
3164
  var PRICE_SCALE_DECIMAL_PLACES = 8;
3108
3165
  var QuoteModule = class {
@@ -3160,11 +3217,12 @@ var QuoteModule = class {
3160
3217
  message: `Price not available for collateral asset: ${collateralPool.asset}`
3161
3218
  });
3162
3219
  }
3163
- if (request.borrowAmount <= 0n) {
3164
- validationErrors.push({
3165
- code: "BORROW_AMOUNT_TOO_LOW" /* BORROW_AMOUNT_TOO_LOW */,
3166
- message: "Borrow amount must be greater than 0"
3167
- });
3220
+ const borrowAmountError = createBorrowAmountValidationError({
3221
+ amount: request.borrowAmount,
3222
+ asset: borrowPool.asset
3223
+ });
3224
+ if (borrowAmountError) {
3225
+ validationErrors.push(borrowAmountError);
3168
3226
  }
3169
3227
  if (request.collateralAmount <= 0n) {
3170
3228
  validationErrors.push({
@@ -3286,16 +3344,12 @@ var QuoteModule = class {
3286
3344
  message: `Price not available for collateral asset: ${collateralAsset}`
3287
3345
  });
3288
3346
  }
3289
- if (borrowAmount < 0n) {
3290
- validationErrors.push({
3291
- code: "BORROW_AMOUNT_TOO_LOW" /* BORROW_AMOUNT_TOO_LOW */,
3292
- message: `Borrow amount must be non-negative`
3293
- });
3294
- } else if (borrowAmount < MIN_BORROW_AMOUNT_BASE_UNITS) {
3295
- validationErrors.push({
3296
- code: "BORROW_AMOUNT_TOO_LOW" /* BORROW_AMOUNT_TOO_LOW */,
3297
- message: `Borrow amount must be at least ${MIN_BORROW_AMOUNT_BASE_UNITS} base units`
3298
- });
3347
+ const borrowAmountError = createBorrowAmountValidationError({
3348
+ amount: borrowAmount,
3349
+ asset: borrowAsset
3350
+ });
3351
+ if (borrowAmountError) {
3352
+ validationErrors.push(borrowAmountError);
3299
3353
  }
3300
3354
  if (targetLtvBps <= MIN_LTV_BPS) {
3301
3355
  validationErrors.push({
@@ -3418,6 +3472,22 @@ function createQuoteResult(params) {
3418
3472
  warnings: params.warnings
3419
3473
  };
3420
3474
  }
3475
+ function createBorrowAmountValidationError(params) {
3476
+ if (params.amount <= 0n) {
3477
+ return {
3478
+ code: "BORROW_AMOUNT_TOO_LOW" /* BORROW_AMOUNT_TOO_LOW */,
3479
+ message: "Borrow amount must be greater than 0"
3480
+ };
3481
+ }
3482
+ const minimumBorrowAmountError = getBorrowAmountMinimumValidationError(params);
3483
+ if (!minimumBorrowAmountError) {
3484
+ return null;
3485
+ }
3486
+ return {
3487
+ code: "BORROW_AMOUNT_TOO_LOW" /* BORROW_AMOUNT_TOO_LOW */,
3488
+ message: minimumBorrowAmountError.message
3489
+ };
3490
+ }
3421
3491
  function computeUsdInternalFromBaseUnits(params) {
3422
3492
  const { amountBaseUnits, priceScaled, assetDecimalPlaces } = params;
3423
3493
  const scaleDiff = INTERNAL_USD_DECIMAL_PLACES - PRICE_SCALE_DECIMAL_PLACES;
@@ -3460,6 +3530,82 @@ function formatBpsAsPercent(bps) {
3460
3530
  const padded = fractional.toString().padStart(2, "0").replace(/0+$/, "");
3461
3531
  return `${whole}.${padded}`;
3462
3532
  }
3533
+ var BTC_MAINNET_ADDRESS_ERROR = "Address must be a valid mainnet BTC address";
3534
+ var EVM_ADDRESS_ERROR = "Address must be a valid EVM address";
3535
+ var ADDRESS_CHAIN_MISMATCH_ERROR = "Address chain must match asset";
3536
+ function normalizeExternalAddress(params) {
3537
+ if (params.asset === Asset.BTC) {
3538
+ if (params.chain !== Chain.BTC) {
3539
+ throw new LiquidiumError(
3540
+ LiquidiumErrorCode.INVALID_ADDRESS,
3541
+ ADDRESS_CHAIN_MISMATCH_ERROR
3542
+ );
3543
+ }
3544
+ if (!validate(params.address, Network.mainnet)) {
3545
+ throw new LiquidiumError(
3546
+ LiquidiumErrorCode.INVALID_ADDRESS,
3547
+ BTC_MAINNET_ADDRESS_ERROR
3548
+ );
3549
+ }
3550
+ return params.address;
3551
+ }
3552
+ if (isEthStablecoin2(params.asset)) {
3553
+ if (params.chain !== Chain.ETH) {
3554
+ throw new LiquidiumError(
3555
+ LiquidiumErrorCode.INVALID_ADDRESS,
3556
+ ADDRESS_CHAIN_MISMATCH_ERROR
3557
+ );
3558
+ }
3559
+ if (!isAddress(params.address)) {
3560
+ throw new LiquidiumError(
3561
+ LiquidiumErrorCode.INVALID_ADDRESS,
3562
+ EVM_ADDRESS_ERROR
3563
+ );
3564
+ }
3565
+ return getAddress(params.address);
3566
+ }
3567
+ return params.address;
3568
+ }
3569
+ function normalizeAndValidateEvmAddress(address, errorMessage) {
3570
+ if (!isAddress(address)) {
3571
+ throw new LiquidiumError(LiquidiumErrorCode.INVALID_ADDRESS, errorMessage);
3572
+ }
3573
+ return getAddress(address);
3574
+ }
3575
+ function isEthStablecoin2(asset) {
3576
+ return asset === Asset.USDC || asset === Asset.USDT;
3577
+ }
3578
+
3579
+ // src/modules/instant-loans/_internal/address-validation.ts
3580
+ function validateInstantLoanBorrowDestination(address, asset) {
3581
+ return normalizeExternalAddress({
3582
+ address,
3583
+ asset,
3584
+ chain: getChainForInstantLoanAsset(asset)
3585
+ });
3586
+ }
3587
+ function validateInstantLoanRefundDestination(address, asset) {
3588
+ return normalizeExternalAddress({
3589
+ address,
3590
+ asset,
3591
+ chain: getChainForInstantLoanAsset(asset)
3592
+ });
3593
+ }
3594
+ function getChainForInstantLoanAsset(asset) {
3595
+ if (asset === Asset.BTC) return Chain.BTC;
3596
+ if (asset === Asset.USDC || asset === Asset.USDT) return Chain.ETH;
3597
+ return asset;
3598
+ }
3599
+
3600
+ // src/modules/instant-loans/types.ts
3601
+ var InstantLoanStatus = {
3602
+ awaitingDeposit: "awaiting_deposit",
3603
+ depositDetected: "deposit_detected",
3604
+ active: "active",
3605
+ settling: "settling",
3606
+ closed: "closed",
3607
+ expired: "expired"
3608
+ };
3463
3609
 
3464
3610
  // src/modules/instant-loans/instant-loans.ts
3465
3611
  var REPAYMENT_BUFFER_SECONDS = 86400n;
@@ -3480,7 +3626,7 @@ var InstantLoansModule = class {
3480
3626
  positions;
3481
3627
  /**
3482
3628
  * Creates a profileless instant loan and returns canonical canister state plus
3483
- * deposit/repay targets for the generated lending profile.
3629
+ * generated initial-deposit and repayment quote targets.
3484
3630
  *
3485
3631
  * Choose `collateralPoolId` and `borrowPoolId` from
3486
3632
  * `client.market.listPools()`, convert UI amounts to base units with the
@@ -3493,10 +3639,22 @@ var InstantLoansModule = class {
3493
3639
  * SDK maps it to the canister's internal `ltv_timer_s` field.
3494
3640
  *
3495
3641
  * @param request - Pool ids, assets, base-unit amounts, LTV limit, timeout, and destinations.
3496
- * @returns Hydrated loan state plus generated deposit and repayment targets.
3642
+ * @returns Hydrated loan state plus generated initial-deposit and repayment quote targets.
3497
3643
  */
3498
3644
  async create(request) {
3499
3645
  validateCreateRequest(request);
3646
+ const borrowDestination = {
3647
+ External: validateInstantLoanBorrowDestination(
3648
+ addressFromAccountInput(request.borrowDestination),
3649
+ request.borrowAsset
3650
+ )
3651
+ };
3652
+ const refundDestination = {
3653
+ External: validateInstantLoanRefundDestination(
3654
+ addressFromAccountInput(request.refundDestination),
3655
+ request.collateralAsset
3656
+ )
3657
+ };
3500
3658
  const apiClient = this.requireApi("Instant loan creation");
3501
3659
  await this.validateInstantLoanLtvPolicy(request);
3502
3660
  const response = await apiClient.post(SdkApiPath.instantLoans, {
@@ -3508,17 +3666,10 @@ var InstantLoansModule = class {
3508
3666
  borrowAmount: request.borrowAmount.toString(),
3509
3667
  ltvMaxBps: request.ltvMaxBps.toString(),
3510
3668
  depositWindowSeconds: request.depositWindowSeconds.toString(),
3511
- borrowDestination: accountWireFromInput(request.borrowDestination),
3512
- refundDestination: accountWireFromInput(request.refundDestination)
3669
+ borrowDestination,
3670
+ refundDestination
3513
3671
  });
3514
- const loan = await this.mapLoanWire(response.loan);
3515
- return {
3516
- ...loan,
3517
- collateral: {
3518
- ...loan.collateral,
3519
- amount: request.collateralAmount
3520
- }
3521
- };
3672
+ return await this.mapLoanWire(response.loan);
3522
3673
  }
3523
3674
  /**
3524
3675
  * Resolves canonical canister state by loan id or short reference.
@@ -3527,7 +3678,7 @@ var InstantLoansModule = class {
3527
3678
  * from the instant-loans canister.
3528
3679
  *
3529
3680
  * @param request - Canister loan id or short public reference.
3530
- * @returns Hydrated loan state plus generated deposit and repayment targets.
3681
+ * @returns Hydrated loan state plus generated initial-deposit and repayment quote targets.
3531
3682
  */
3532
3683
  async get(request) {
3533
3684
  const loanId = "loanId" in request ? request.loanId : decodeRef(request.ref);
@@ -3538,7 +3689,8 @@ var InstantLoansModule = class {
3538
3689
  if ("Err" in result) {
3539
3690
  throw mapInstantLoansErrorToLiquidiumError(result.Err);
3540
3691
  }
3541
- return await this.mapLoanRecord(result.Ok);
3692
+ const collateralAmount = await this.getCollateralAmountHint(loanId);
3693
+ return await this.mapLoanRecord(result.Ok, collateralAmount);
3542
3694
  } catch (error) {
3543
3695
  if (error instanceof LiquidiumError) {
3544
3696
  throw error;
@@ -3661,25 +3813,34 @@ var InstantLoansModule = class {
3661
3813
  );
3662
3814
  }
3663
3815
  const apiClient = this.requireApi("Instant loan address lookup");
3664
- const response = await apiClient.get(buildInstantLoanAddressLookupPath({ address: trimmedAddress }));
3816
+ const response = await apiClient.get(
3817
+ buildInstantLoanAddressLookupPath({ address: trimmedAddress })
3818
+ );
3665
3819
  return (response.candidates ?? response.loans ?? []).map(mapCandidateWire);
3666
3820
  }
3667
- async mapLoanRecord(record) {
3821
+ async mapLoanRecord(record, collateralAmount) {
3668
3822
  return await this.hydrateLoan({
3669
3823
  loanId: record.id,
3670
3824
  profileId: record.lending_profile.toText(),
3671
3825
  ltvMaxBps: record.ltv_max_bps,
3672
3826
  depositWindowSeconds: record.ltv_timer_s,
3673
3827
  collateralPoolId: record.lend_pool_id.toText(),
3828
+ collateralAmount,
3674
3829
  borrowPoolId: record.borrow_pool_id.toText(),
3675
3830
  collateralAsset: getVariantKey(record.lend_asset),
3676
3831
  borrowAsset: getVariantKey(record.borrow_asset),
3677
3832
  borrowAmount: record.borrow_amount,
3678
3833
  borrowDestination: accountFromCanister(record.borrow_destination),
3679
- refundDestination: accountFromCanister(record.refund_destination),
3680
- started: record.started
3834
+ refundDestination: accountFromCanister(record.refund_destination)
3681
3835
  });
3682
3836
  }
3837
+ async getCollateralAmountHint(loanId) {
3838
+ const apiClient = this.requireApi("Instant loan lookup");
3839
+ const response = await apiClient.get(
3840
+ buildInstantLoanCollateralHintPath({ loanId })
3841
+ );
3842
+ return parseBigintWire(response.collateralAmountHint, "collateral amount");
3843
+ }
3683
3844
  async mapLoanWire(loan) {
3684
3845
  const loanId = parseBigintWire(loan.loanId, "loan ID");
3685
3846
  return await this.hydrateLoan({
@@ -3691,6 +3852,10 @@ var InstantLoansModule = class {
3691
3852
  "deposit window"
3692
3853
  ),
3693
3854
  collateralPoolId: loan.collateral.poolId,
3855
+ collateralAmount: parseBigintWire(
3856
+ loan.collateral.amountHint,
3857
+ "collateral amount"
3858
+ ),
3694
3859
  borrowPoolId: loan.borrow.poolId,
3695
3860
  collateralAsset: loan.collateral.asset,
3696
3861
  borrowAsset: loan.borrow.asset,
@@ -3735,54 +3900,64 @@ var InstantLoansModule = class {
3735
3900
  );
3736
3901
  const repaymentInflowFee = totalDebtAmount > 0n ? await this.estimateRepaymentInflowFee(borrowAsset, repayTarget.chain) : { totalFee: 0n, estimateAvailable: false };
3737
3902
  const repaymentAmount = totalDebtAmount + interestBufferAmount + repaymentInflowFee.totalFee;
3738
- const collateralAmount = collateralPosition?.deposited ?? 0n;
3739
- const collateralDecimals = collateralPosition?.depositedDecimals ?? 0n;
3903
+ const currentCollateralAmount = collateralPosition?.deposited ?? 0n;
3904
+ const collateralAmount = input.collateralAmount;
3905
+ const collateralDecimals = collateralPosition?.depositedDecimals ?? getAssetNativeDecimals(collateralAsset);
3740
3906
  const collateralInterestAmount = collateralPosition?.earnedInterest ?? 0n;
3741
3907
  const borrowedAmount = borrowPosition?.borrowed ?? 0n;
3742
- const borrowedDecimals = borrowPosition?.borrowedDecimals ?? 0n;
3908
+ const borrowedDecimals = borrowPosition?.borrowedDecimals ?? getAssetNativeDecimals(borrowAsset);
3743
3909
  const debtInterestAmount = borrowPosition?.debtInterest ?? 0n;
3744
3910
  const status = deriveInstantLoanStatus({
3745
- collateralAmount,
3746
- started: input.started,
3911
+ collateralAmount: currentCollateralAmount,
3747
3912
  totalDebtAmount
3748
3913
  });
3914
+ const initialDeposit = await this.createInitialDepositQuote({
3915
+ collateralAmount,
3916
+ decimals: collateralDecimals,
3917
+ asset: collateralAsset,
3918
+ target: depositTarget
3919
+ });
3920
+ const repayment = {
3921
+ amount: repaymentAmount,
3922
+ decimals: borrowedDecimals,
3923
+ debtAmount: totalDebtAmount,
3924
+ interestBufferAmount,
3925
+ interestBufferSeconds: REPAYMENT_BUFFER_SECONDS,
3926
+ inflowFeeAmount: repaymentInflowFee.totalFee,
3927
+ inflowFeeEstimateAvailable: repaymentInflowFee.estimateAvailable,
3928
+ asset: borrowAsset,
3929
+ chain: repayTarget.chain,
3930
+ target: repayTarget
3931
+ };
3749
3932
  return {
3750
3933
  loanId: input.loanId,
3751
3934
  ref: publicIdFromInt(input.loanId),
3752
3935
  status,
3753
3936
  profileId,
3754
- ltvMaxBps: input.ltvMaxBps,
3755
- depositWindowSeconds: input.depositWindowSeconds,
3937
+ terms: {
3938
+ ltvMaxBps: input.ltvMaxBps,
3939
+ depositWindowSeconds: input.depositWindowSeconds
3940
+ },
3756
3941
  collateral: {
3757
3942
  poolId: collateralPoolId,
3758
3943
  asset: collateralAsset,
3759
3944
  chain: depositTarget.chain,
3945
+ decimals: collateralDecimals,
3760
3946
  amount: collateralAmount
3761
3947
  },
3762
3948
  borrow: {
3763
3949
  poolId: borrowPoolId,
3764
3950
  asset: borrowAsset,
3765
3951
  chain: repayTarget.chain,
3952
+ decimals: borrowedDecimals,
3766
3953
  amount: input.borrowAmount,
3767
3954
  destination: input.borrowDestination
3768
3955
  },
3769
3956
  refundDestination: input.refundDestination,
3770
- depositTarget,
3771
- repayTarget,
3772
- repayment: {
3773
- amount: repaymentAmount,
3774
- decimals: borrowedDecimals,
3775
- debtAmount: totalDebtAmount,
3776
- interestBufferAmount,
3777
- interestBufferSeconds: REPAYMENT_BUFFER_SECONDS,
3778
- inflowFeeAmount: repaymentInflowFee.totalFee,
3779
- inflowFeeEstimateAvailable: repaymentInflowFee.estimateAvailable,
3780
- asset: borrowAsset,
3781
- chain: repayTarget.chain,
3782
- target: repayTarget
3783
- },
3957
+ initialDeposit,
3958
+ repayment,
3784
3959
  position: {
3785
- collateralAmount,
3960
+ collateralAmount: currentCollateralAmount,
3786
3961
  collateralDecimals,
3787
3962
  collateralInterestAmount,
3788
3963
  borrowedAmount,
@@ -3792,6 +3967,21 @@ var InstantLoansModule = class {
3792
3967
  }
3793
3968
  };
3794
3969
  }
3970
+ async createInitialDepositQuote(input) {
3971
+ const inflowFee = await this.lending.estimateInflowFee({
3972
+ asset: input.asset,
3973
+ chain: input.target.chain
3974
+ });
3975
+ return {
3976
+ amount: input.collateralAmount + inflowFee.totalFee,
3977
+ decimals: input.decimals,
3978
+ collateralAmount: input.collateralAmount,
3979
+ inflowFeeAmount: inflowFee.totalFee,
3980
+ asset: input.asset,
3981
+ chain: input.target.chain,
3982
+ target: input.target
3983
+ };
3984
+ }
3795
3985
  async estimateRepaymentInflowFee(asset, chain) {
3796
3986
  try {
3797
3987
  const fee = await this.lending.estimateInflowFee({
@@ -3878,15 +4068,12 @@ function calculateTotalDebtAmount(borrowPosition) {
3878
4068
  }
3879
4069
  function deriveInstantLoanStatus(input) {
3880
4070
  if (input.totalDebtAmount > 0n) {
3881
- return "active";
4071
+ return InstantLoanStatus.active;
3882
4072
  }
3883
4073
  if (input.collateralAmount > 0n) {
3884
- return input.started ? "settling" : "deposit_detected";
4074
+ return InstantLoanStatus.depositDetected;
3885
4075
  }
3886
- if (input.started) {
3887
- return "closed";
3888
- }
3889
- return "awaiting_deposit";
4076
+ return InstantLoanStatus.awaitingDeposit;
3890
4077
  }
3891
4078
  function validateCreateRequest(request) {
3892
4079
  if (request.collateralAmount <= 0n) {
@@ -3920,7 +4107,7 @@ function throwLtvCalculationError(error) {
3920
4107
  error?.message ?? "Unable to calculate instant loan LTV"
3921
4108
  );
3922
4109
  }
3923
- function accountWireFromInput(account) {
4110
+ function addressFromAccountInput(account) {
3924
4111
  const address = typeof account === "string" ? account.trim() : account.address.trim();
3925
4112
  if (!address) {
3926
4113
  throw new LiquidiumError(
@@ -3928,7 +4115,7 @@ function accountWireFromInput(account) {
3928
4115
  "Instant loan account address must be non-empty"
3929
4116
  );
3930
4117
  }
3931
- return { External: address };
4118
+ return address;
3932
4119
  }
3933
4120
  function accountFromCanister(account) {
3934
4121
  if ("Native" in account) {
@@ -4094,19 +4281,20 @@ function mapCandidateWire(wire) {
4094
4281
  wire.borrowAsset ?? wire.borrow_asset,
4095
4282
  "borrow asset"
4096
4283
  ),
4097
- collateralAmountHint: parseBigintWire(
4098
- wire.collateralAmountHint ?? wire.min_deposit_hint,
4099
- "collateral amount hint"
4284
+ collateralAmount: parseBigintWire(
4285
+ wire.collateralAmount,
4286
+ "collateral amount"
4100
4287
  )
4101
4288
  };
4102
4289
  }
4103
4290
  function parseBigintWire(value, label) {
4104
- if (typeof value === "bigint") return value;
4105
- if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value);
4106
- throw new LiquidiumError(
4107
- LiquidiumErrorCode.VALIDATION_ERROR,
4108
- `Invalid instant loan ${label}`
4109
- );
4291
+ if (!value || !/^\d+$/.test(value)) {
4292
+ throw new LiquidiumError(
4293
+ LiquidiumErrorCode.VALIDATION_ERROR,
4294
+ `Invalid instant loan ${label}`
4295
+ );
4296
+ }
4297
+ return BigInt(value);
4110
4298
  }
4111
4299
  function requiredString(value, label) {
4112
4300
  if (value?.trim()) return value;
@@ -4177,15 +4365,6 @@ function formatBpsAsPercent2(value) {
4177
4365
  const fractionalPart = (absoluteValue % 100n).toString().padStart(2, "0");
4178
4366
  return `${sign}${wholePart.toString()}.${fractionalPart}%`;
4179
4367
  }
4180
-
4181
- // src/modules/instant-loans/types.ts
4182
- var InstantLoanStatus = {
4183
- awaitingDeposit: "awaiting_deposit",
4184
- depositDetected: "deposit_detected",
4185
- active: "active",
4186
- settling: "settling",
4187
- closed: "closed"
4188
- };
4189
4368
  function createApproveTransaction(params) {
4190
4369
  return {
4191
4370
  to: params.tokenAddress,
@@ -4262,6 +4441,16 @@ function encodeBytes32Hex(bytes) {
4262
4441
  (byte) => byte.toString(16).padStart(2, "0")
4263
4442
  ).join("")}`;
4264
4443
  }
4444
+ var idlFactory5 = ({ IDL }) => IDL.Service({
4445
+ icrc1_fee: IDL.Func([], [IDL.Nat], ["query"])
4446
+ });
4447
+ function createCkBtcLedgerActor(canisterContext) {
4448
+ const canisterId = CK_CANISTER_IDS.btcLedger;
4449
+ return Actor.createActor(idlFactory5, {
4450
+ agent: canisterContext.agent,
4451
+ canisterId
4452
+ });
4453
+ }
4265
4454
 
4266
4455
  // src/core/canisters/lending/messages.ts
4267
4456
  function createBorrowAssetMessage(request, nonce) {
@@ -4344,6 +4533,28 @@ function assertValidRetryOptions(options) {
4344
4533
  }
4345
4534
  }
4346
4535
 
4536
+ // src/modules/lending/_internal/inflow-fee-rounding.ts
4537
+ var ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS = 100000n;
4538
+ var BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS = 500n;
4539
+ function roundInflowFeeEstimate(request, totalFee) {
4540
+ if (totalFee <= 0n) {
4541
+ return 0n;
4542
+ }
4543
+ if (isEthStablecoin(request.asset, request.chain)) {
4544
+ return roundUpToNearest(
4545
+ totalFee,
4546
+ ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS
4547
+ );
4548
+ }
4549
+ if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
4550
+ return roundUpToNearest(totalFee, BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS);
4551
+ }
4552
+ return totalFee;
4553
+ }
4554
+ function roundUpToNearest(amount, roundingUnit) {
4555
+ return ceilDivBigint(amount, roundingUnit) * roundingUnit;
4556
+ }
4557
+
4347
4558
  // src/modules/lending/mappers.ts
4348
4559
  function mapCanisterOutflowDetails(outflow) {
4349
4560
  const rawOutflowType = getVariantKey(outflow.outflow_type);
@@ -4432,6 +4643,10 @@ var LendingModule = class {
4432
4643
  "Withdraw requires a signer account"
4433
4644
  );
4434
4645
  }
4646
+ const receiverAddress = await this.normalizeOutflowReceiverAddress({
4647
+ poolId: request.poolId,
4648
+ receiverAddress: destinationAccount
4649
+ });
4435
4650
  const lendingActor = createLendingActor(this.canisterContext);
4436
4651
  try {
4437
4652
  const expiryTimestamp = computeExpiryTimestampFromNow();
@@ -4440,7 +4655,7 @@ var LendingModule = class {
4440
4655
  profileId: request.profileId,
4441
4656
  poolId: request.poolId,
4442
4657
  amount: request.amount,
4443
- receiverAddress: destinationAccount,
4658
+ receiverAddress,
4444
4659
  signerWalletAddress: signerAccount,
4445
4660
  expiryTimestamp
4446
4661
  };
@@ -4454,7 +4669,7 @@ var LendingModule = class {
4454
4669
  {
4455
4670
  pool_id: request.poolId,
4456
4671
  amount: request.amount.toString(),
4457
- account: { type: "External", data: destinationAccount },
4672
+ account: { type: "External", data: receiverAddress },
4458
4673
  expiry_timestamp: expiryTimestamp
4459
4674
  },
4460
4675
  nonce
@@ -4547,6 +4762,29 @@ var LendingModule = class {
4547
4762
  "Borrow requires a signer account"
4548
4763
  );
4549
4764
  }
4765
+ if (request.amount <= 0n) {
4766
+ throw new LiquidiumError(
4767
+ LiquidiumErrorCode.VALIDATION_ERROR,
4768
+ "Borrow amount must be greater than 0"
4769
+ );
4770
+ }
4771
+ const selectedPool = await this.getPoolById(request.poolId);
4772
+ const selectedAsset = getVariantKey(selectedPool.asset);
4773
+ const minimumBorrowAmountError = getBorrowAmountMinimumValidationError({
4774
+ amount: request.amount,
4775
+ asset: selectedAsset
4776
+ });
4777
+ if (minimumBorrowAmountError) {
4778
+ throw new LiquidiumError(
4779
+ LiquidiumErrorCode.VALIDATION_ERROR,
4780
+ minimumBorrowAmountError.message
4781
+ );
4782
+ }
4783
+ const receiverAddress = normalizeExternalAddress({
4784
+ address: destinationAccount,
4785
+ asset: selectedAsset,
4786
+ chain: getVariantKey(selectedPool.chain)
4787
+ });
4550
4788
  const lendingActor = createLendingActor(this.canisterContext);
4551
4789
  try {
4552
4790
  const expiryTimestamp = computeExpiryTimestampFromNow();
@@ -4555,7 +4793,7 @@ var LendingModule = class {
4555
4793
  profileId: request.profileId,
4556
4794
  poolId: request.poolId,
4557
4795
  amount: request.amount,
4558
- receiverAddress: destinationAccount,
4796
+ receiverAddress,
4559
4797
  signerWalletAddress: signerAccount,
4560
4798
  expiryTimestamp
4561
4799
  };
@@ -4569,7 +4807,7 @@ var LendingModule = class {
4569
4807
  {
4570
4808
  pool_id: request.poolId,
4571
4809
  amount: request.amount.toString(),
4572
- account: { type: "External", data: destinationAccount },
4810
+ account: { type: "External", data: receiverAddress },
4573
4811
  expiry_timestamp: expiryTimestamp
4574
4812
  },
4575
4813
  nonce
@@ -4818,10 +5056,10 @@ var LendingModule = class {
4818
5056
  * Estimates the network/deposit fee for an inflow target.
4819
5057
  *
4820
5058
  * ETH stablecoin deposit-address estimates are served by the deposit-address
4821
- * canister. BTC estimates are not exposed by this SDK yet and return zero.
5059
+ * canister. BTC estimates include the ckBTC minter deposit fee and ledger fee.
4822
5060
  *
4823
5061
  * @param request - Asset and chain pair to estimate for.
4824
- * @returns Total fee estimate in the asset's base units.
5062
+ * @returns Total fee estimate rounded up in the asset's base units.
4825
5063
  */
4826
5064
  async estimateInflowFee(request) {
4827
5065
  if (isEthStablecoin(request.asset, request.chain)) {
@@ -4831,16 +5069,28 @@ var LendingModule = class {
4831
5069
  if ("Err" in result) {
4832
5070
  throw mapDepositAccountErrorToLiquidiumError(result.Err);
4833
5071
  }
4834
- return { totalFee: result.Ok };
5072
+ return {
5073
+ totalFee: roundInflowFeeEstimate(request, result.Ok)
5074
+ };
4835
5075
  }
4836
5076
  if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
4837
- return { totalFee: 0n };
5077
+ const estimate = await this.estimateBtcInflowFee();
5078
+ return {
5079
+ totalFee: roundInflowFeeEstimate(request, estimate.totalFee)
5080
+ };
4838
5081
  }
4839
5082
  throw new LiquidiumError(
4840
5083
  LiquidiumErrorCode.VALIDATION_ERROR,
4841
5084
  `Inflow fee estimates are not supported for ${request.asset} on ${request.chain}`
4842
5085
  );
4843
5086
  }
5087
+ async estimateBtcInflowFee() {
5088
+ const [minterFee, ledgerFee] = await Promise.all([
5089
+ createCkBtcMinterActor(this.canisterContext).get_deposit_fee(),
5090
+ createCkBtcLedgerActor(this.canisterContext).icrc1_fee()
5091
+ ]);
5092
+ return { totalFee: minterFee + ledgerFee };
5093
+ }
4844
5094
  async sendAndSubmitNativeSupplyInflow(params) {
4845
5095
  const { request, instruction, defaultSubmitInflowRequest } = params;
4846
5096
  if (instruction.target.type !== "nativeAddress") {
@@ -5133,6 +5383,14 @@ var LendingModule = class {
5133
5383
  }
5134
5384
  return selectedPool;
5135
5385
  }
5386
+ async normalizeOutflowReceiverAddress(params) {
5387
+ const selectedPool = await this.getPoolById(params.poolId);
5388
+ return normalizeExternalAddress({
5389
+ address: params.receiverAddress,
5390
+ asset: getVariantKey(selectedPool.asset),
5391
+ chain: getVariantKey(selectedPool.chain)
5392
+ });
5393
+ }
5136
5394
  async sendEthContractTransaction(walletAdapter, walletAddress, request, actionType) {
5137
5395
  if (!walletAdapter.sendEthTransaction) {
5138
5396
  throw new LiquidiumError(
@@ -5207,12 +5465,6 @@ function getApprovalStrategy(params) {
5207
5465
  }
5208
5466
  return EvmSupplyApprovalStrategy.resetThenApproveMax;
5209
5467
  }
5210
- function normalizeAndValidateEvmAddress(address, errorMessage) {
5211
- if (!isAddress(address)) {
5212
- throw new LiquidiumError(LiquidiumErrorCode.INVALID_ADDRESS, errorMessage);
5213
- }
5214
- return getAddress(address);
5215
- }
5216
5468
  function mapExpectedOutflowDetails(details, expectedOutflowType, operation) {
5217
5469
  if (details.outflowType !== expectedOutflowType) {
5218
5470
  throw new LiquidiumError(
@@ -5262,24 +5514,6 @@ function shouldSubmitInflow(params) {
5262
5514
  return !isEthStablecoin(params.instruction.asset, params.instruction.chain);
5263
5515
  }
5264
5516
 
5265
- // src/core/utils/asset-decimals.ts
5266
- var ASSET_NATIVE_DECIMALS = {
5267
- BTC: 8n,
5268
- USDC: 6n,
5269
- USDT: 6n,
5270
- SOL: 9n
5271
- };
5272
- function getAssetNativeDecimals(asset) {
5273
- const decimals = ASSET_NATIVE_DECIMALS[asset];
5274
- if (decimals === void 0) {
5275
- throw new LiquidiumError(
5276
- LiquidiumErrorCode.VALIDATION_ERROR,
5277
- `Native decimals are not configured for asset: ${asset}`
5278
- );
5279
- }
5280
- return decimals;
5281
- }
5282
-
5283
5517
  // src/modules/market/mappers.ts
5284
5518
  var DECIMAL_BASE = 10;
5285
5519
  var PAIR_SEPARATOR = "_";
@@ -5798,6 +6032,6 @@ function resolveEvmReadClient(config) {
5798
6032
  });
5799
6033
  }
5800
6034
 
5801
- export { AccountsModule, ActivitiesModule, ActivityDirection, ActivityFilter, ActivityKind, ActivityStatus, Asset, CK_ETH_DEPOSIT_CONTRACT_ADDRESS, Chain, Environment, EvmSupplyApprovalStrategy, HistoryModule, InflowSubmitType, InstantLoanStatus, InstantLoansModule, LendingModule, LiquidiumClient, LiquidiumError, LiquidiumErrorCode, MarketModule, OutflowType, PositionsModule, QuoteModule, QuoteValidationErrorCode, QuoteWarningCode, RATE_DECIMALS, RATE_SCALE, SupplyAction, SupplyPlanType, TransferMode, USDC_CONTRACT_ADDRESS, USDT_CONTRACT_ADDRESS, UserHistoryStatus, WalletExecutionKind, createTransferErc20Transaction, executeWith, intFromPublicId, publicIdFromInt };
6035
+ export { AccountsModule, ActivitiesModule, ActivityDirection, ActivityFilter, ActivityKind, ActivityStatus, Asset, CK_ETH_DEPOSIT_CONTRACT_ADDRESS, Chain, Environment, EvmSupplyApprovalStrategy, HistoryModule, InflowSubmitType, InstantLoanStatus, InstantLoansModule, LendingModule, LiquidiumClient, LiquidiumError, LiquidiumErrorCode, MIN_BORROW_AMOUNTS_BY_ASSET, MarketModule, OutflowType, PositionsModule, QuoteModule, QuoteValidationErrorCode, QuoteWarningCode, RATE_DECIMALS, RATE_SCALE, SupplyAction, SupplyPlanType, TransferMode, USDC_CONTRACT_ADDRESS, USDT_CONTRACT_ADDRESS, UserHistoryStatus, WalletExecutionKind, createTransferErc20Transaction, executeWith, getMinimumBorrowAmount, intFromPublicId, publicIdFromInt };
5802
6036
  //# sourceMappingURL=index.js.map
5803
6037
  //# sourceMappingURL=index.js.map