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