@liquidium/client 0.2.1 → 0.3.0

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
@@ -1591,9 +1591,9 @@ function buildActivityStatusPath(request) {
1591
1591
  request.id
1592
1592
  )}/status?${query.toString()}`;
1593
1593
  }
1594
- function buildInstantLoanAddressLookupPath(request) {
1595
- const query = new URLSearchParams({ address: request.address });
1596
- return `${INSTANT_LOANS}/address?${query.toString()}`;
1594
+ function buildInstantLoanFindPath(request) {
1595
+ const query = new URLSearchParams({ query: request.query });
1596
+ return `${INSTANT_LOANS}/find?${query.toString()}`;
1597
1597
  }
1598
1598
  function buildInstantLoanCollateralHintPath(request) {
1599
1599
  return `${INSTANT_LOANS}/${encodeURIComponent(
@@ -2222,6 +2222,41 @@ function mapHistoryStatusToApi(status) {
2222
2222
  return status.toUpperCase();
2223
2223
  }
2224
2224
 
2225
+ // src/core/utils/api-response-parsers.ts
2226
+ var MILLISECONDS_PER_SECOND2 = 1e3;
2227
+ function parseNonEmptyApiString(value, field) {
2228
+ if (!value) {
2229
+ throwInvalidApiResponseField(field);
2230
+ }
2231
+ return value;
2232
+ }
2233
+ function parseUnsignedApiBigint(value, field) {
2234
+ if (!value || !/^\d+$/.test(value)) {
2235
+ throwInvalidApiResponseField(field);
2236
+ }
2237
+ return BigInt(value);
2238
+ }
2239
+ function parseIsoApiTimestampToUnixSeconds(value, field) {
2240
+ const timestamp = parseNonEmptyApiString(value, field);
2241
+ const timestampMilliseconds = Date.parse(timestamp);
2242
+ if (!Number.isFinite(timestampMilliseconds)) {
2243
+ throwInvalidApiResponseField(field);
2244
+ }
2245
+ return BigInt(Math.floor(timestampMilliseconds / MILLISECONDS_PER_SECOND2));
2246
+ }
2247
+ function parseApiStringUnion(value, allowedValues, field) {
2248
+ if (value !== void 0 && allowedValues.includes(value)) {
2249
+ return value;
2250
+ }
2251
+ throwInvalidApiResponseField(field);
2252
+ }
2253
+ function throwInvalidApiResponseField(field) {
2254
+ throw new LiquidiumError(
2255
+ LiquidiumErrorCode.VALIDATION_ERROR,
2256
+ `Invalid ${field.context} ${field.label}`
2257
+ );
2258
+ }
2259
+
2225
2260
  // src/core/utils/asset-decimals.ts
2226
2261
  var ASSET_NATIVE_DECIMALS = {
2227
2262
  BTC: 8n,
@@ -3613,6 +3648,14 @@ var RATE_SCALE2 = 10n ** 27n;
3613
3648
  var SECONDS_PER_YEAR = 31536000n;
3614
3649
  var ETH_STABLECOIN_INFLOW_FEE_FALLBACK = 1500000n;
3615
3650
  var INSTANT_LOAN_MIN_SLIPPAGE_BUFFER_BPS = 200n;
3651
+ var INSTANT_LOAN_FIND_QUERY_MAX_LENGTH = 256;
3652
+ var INSTANT_LOAN_WIRE_CONTEXT = "instant loan";
3653
+ var INSTANT_LOAN_ASSETS = [
3654
+ Asset.BTC,
3655
+ Asset.SOL,
3656
+ Asset.USDC,
3657
+ Asset.USDT
3658
+ ];
3616
3659
  var InstantLoansModule = class {
3617
3660
  constructor(canisterContext, apiClient, lending, positions) {
3618
3661
  this.canisterContext = canisterContext;
@@ -3669,7 +3712,19 @@ var InstantLoansModule = class {
3669
3712
  borrowDestination,
3670
3713
  refundDestination
3671
3714
  });
3672
- return await this.mapLoanWire(response.loan);
3715
+ const loanId = parseUnsignedApiBigint(response.loan.loanId, {
3716
+ context: INSTANT_LOAN_WIRE_CONTEXT,
3717
+ label: "loan ID"
3718
+ });
3719
+ const collateralAmount = parseUnsignedApiBigint(
3720
+ response.loan.collateral.amountHint,
3721
+ {
3722
+ context: INSTANT_LOAN_WIRE_CONTEXT,
3723
+ label: "collateral amount"
3724
+ }
3725
+ );
3726
+ const record = await this.getLoanRecord(loanId);
3727
+ return await this.mapLoanRecord(record, collateralAmount);
3673
3728
  }
3674
3729
  /**
3675
3730
  * Resolves canonical canister state by loan id or short reference.
@@ -3682,21 +3737,30 @@ var InstantLoansModule = class {
3682
3737
  */
3683
3738
  async get(request) {
3684
3739
  const loanId = "loanId" in request ? request.loanId : decodeRef(request.ref);
3685
- try {
3686
- const result = await createInstantLoansActor(
3687
- this.canisterContext
3688
- ).get_loan(loanId);
3689
- if ("Err" in result) {
3690
- throw mapInstantLoansErrorToLiquidiumError(result.Err);
3691
- }
3692
- const collateralAmount = await this.getCollateralAmountHint(loanId);
3693
- return await this.mapLoanRecord(result.Ok, collateralAmount);
3694
- } catch (error) {
3695
- if (error instanceof LiquidiumError) {
3696
- throw error;
3697
- }
3698
- throw mapCanisterCallErrorToLiquidiumError("get_loan", error);
3699
- }
3740
+ const record = await this.getLoanRecord(loanId);
3741
+ const collateralAmount = await this.getCollateralAmountHint(loanId);
3742
+ return await this.mapLoanRecord(record, collateralAmount);
3743
+ }
3744
+ /**
3745
+ * Finds instant loans by short reference, numeric loan id string, address, or transaction id.
3746
+ *
3747
+ * Search returns lightweight matches. Call `get({ loanId })` or `get({ ref })`
3748
+ * when the user selects a match and you need hydrated loan state.
3749
+ *
3750
+ * @param query - Short reference, address, transaction id/hash, or numeric loan id string.
3751
+ * @returns Matching loan ids and references from the search index.
3752
+ */
3753
+ async find(query) {
3754
+ const validatedQuery = validateInstantLoanFindQuery(query);
3755
+ const candidates = await this.findCandidateLoansByQuery(validatedQuery);
3756
+ return uniqueInstantLoanFindCandidates(candidates).map((candidate) => ({
3757
+ loanId: candidate.loanId,
3758
+ ref: candidate.ref,
3759
+ createdAt: candidate.createdAt,
3760
+ collateral: candidate.collateral,
3761
+ borrow: candidate.borrow,
3762
+ profileId: candidate.profileId
3763
+ }));
3700
3764
  }
3701
3765
  /**
3702
3766
  * Returns the active instant-loans canister config via direct query.
@@ -3794,30 +3858,29 @@ var InstantLoansModule = class {
3794
3858
  throw mapCanisterCallErrorToLiquidiumError("list_warmed_profiles", error);
3795
3859
  }
3796
3860
  }
3797
- /**
3798
- * Finds candidate loans associated with an address through the Liquidium SDK
3799
- * API. Returns discovery candidates only; call `get(...)` to hydrate canister state.
3800
- *
3801
- * Candidates are useful for recovery flows where the user knows a borrow or
3802
- * refund address but not the loan reference.
3803
- *
3804
- * @param address - Borrow or refund address to search for.
3805
- * @returns Lightweight loan candidates associated with the address.
3806
- */
3807
- async findByAddress(address) {
3808
- const trimmedAddress = address.trim();
3809
- if (!trimmedAddress) {
3810
- throw new LiquidiumError(
3811
- LiquidiumErrorCode.VALIDATION_ERROR,
3812
- "Address lookup requires a non-empty address"
3813
- );
3814
- }
3815
- const apiClient = this.requireApi("Instant loan address lookup");
3861
+ async findCandidateLoansByQuery(query) {
3862
+ const apiClient = this.requireApi("Instant loan find");
3816
3863
  const response = await apiClient.get(
3817
- buildInstantLoanAddressLookupPath({ address: trimmedAddress })
3864
+ buildInstantLoanFindPath({ query })
3818
3865
  );
3819
3866
  return (response.candidates ?? response.loans ?? []).map(mapCandidateWire);
3820
3867
  }
3868
+ async getLoanRecord(loanId) {
3869
+ try {
3870
+ const result = await createInstantLoansActor(
3871
+ this.canisterContext
3872
+ ).get_loan(loanId);
3873
+ if ("Err" in result) {
3874
+ throw mapInstantLoansErrorToLiquidiumError(result.Err);
3875
+ }
3876
+ return result.Ok;
3877
+ } catch (error) {
3878
+ if (error instanceof LiquidiumError) {
3879
+ throw error;
3880
+ }
3881
+ throw mapCanisterCallErrorToLiquidiumError("get_loan", error);
3882
+ }
3883
+ }
3821
3884
  async mapLoanRecord(record, collateralAmount) {
3822
3885
  return await this.hydrateLoan({
3823
3886
  loanId: record.id,
@@ -3831,37 +3894,22 @@ var InstantLoansModule = class {
3831
3894
  borrowAsset: getVariantKey(record.borrow_asset),
3832
3895
  borrowAmount: record.borrow_amount,
3833
3896
  borrowDestination: accountFromCanister(record.borrow_destination),
3834
- refundDestination: accountFromCanister(record.refund_destination)
3897
+ refundDestination: accountFromCanister(record.refund_destination),
3898
+ depositDetectedTimestamp: record.deposit_detected_ts[0] ?? null,
3899
+ expiryTimestamp: record.expires_at[0] ?? deriveDepositExpiryTimestamp({
3900
+ depositDetectedTimestamp: record.deposit_detected_ts[0] ?? null,
3901
+ depositWindowSeconds: record.ltv_timer_s
3902
+ })
3835
3903
  });
3836
3904
  }
3837
3905
  async getCollateralAmountHint(loanId) {
3838
- const apiClient = this.requireApi("Instant loan lookup");
3906
+ const apiClient = this.requireApi("Instant loan collateral hint");
3839
3907
  const response = await apiClient.get(
3840
3908
  buildInstantLoanCollateralHintPath({ loanId })
3841
3909
  );
3842
- return parseBigintWire(response.collateralAmountHint, "collateral amount");
3843
- }
3844
- async mapLoanWire(loan) {
3845
- const loanId = parseBigintWire(loan.loanId, "loan ID");
3846
- return await this.hydrateLoan({
3847
- loanId,
3848
- profileId: loan.profileId,
3849
- ltvMaxBps: parseBigintWire(loan.ltvMaxBps, "max LTV"),
3850
- depositWindowSeconds: parseBigintWire(
3851
- loan.depositWindowSeconds,
3852
- "deposit window"
3853
- ),
3854
- collateralPoolId: loan.collateral.poolId,
3855
- collateralAmount: parseBigintWire(
3856
- loan.collateral.amountHint,
3857
- "collateral amount"
3858
- ),
3859
- borrowPoolId: loan.borrow.poolId,
3860
- collateralAsset: loan.collateral.asset,
3861
- borrowAsset: loan.borrow.asset,
3862
- borrowAmount: parseBigintWire(loan.borrow.amount, "borrow amount"),
3863
- borrowDestination: accountFromWire(loan.borrow.destination),
3864
- refundDestination: accountFromWire(loan.refundDestination)
3910
+ return parseUnsignedApiBigint(response.collateralAmountHint, {
3911
+ context: INSTANT_LOAN_WIRE_CONTEXT,
3912
+ label: "collateral amount"
3865
3913
  });
3866
3914
  }
3867
3915
  async hydrateLoan(input) {
@@ -3915,7 +3963,9 @@ var InstantLoansModule = class {
3915
3963
  collateralAmount,
3916
3964
  decimals: collateralDecimals,
3917
3965
  asset: collateralAsset,
3918
- target: depositTarget
3966
+ target: depositTarget,
3967
+ detectedTimestamp: input.depositDetectedTimestamp,
3968
+ expiryTimestamp: input.expiryTimestamp
3919
3969
  });
3920
3970
  const repayment = {
3921
3971
  amount: repaymentAmount,
@@ -3979,7 +4029,9 @@ var InstantLoansModule = class {
3979
4029
  inflowFeeAmount: inflowFee.totalFee,
3980
4030
  asset: input.asset,
3981
4031
  chain: input.target.chain,
3982
- target: input.target
4032
+ target: input.target,
4033
+ detectedTimestamp: input.detectedTimestamp,
4034
+ expiryTimestamp: input.expiryTimestamp
3983
4035
  };
3984
4036
  }
3985
4037
  async estimateRepaymentInflowFee(asset, chain) {
@@ -4075,6 +4127,12 @@ function deriveInstantLoanStatus(input) {
4075
4127
  }
4076
4128
  return InstantLoanStatus.awaitingDeposit;
4077
4129
  }
4130
+ function deriveDepositExpiryTimestamp(input) {
4131
+ if (input.depositDetectedTimestamp === null) {
4132
+ return null;
4133
+ }
4134
+ return input.depositDetectedTimestamp + input.depositWindowSeconds;
4135
+ }
4078
4136
  function validateCreateRequest(request) {
4079
4137
  if (request.collateralAmount <= 0n) {
4080
4138
  throw new LiquidiumError(
@@ -4123,18 +4181,6 @@ function accountFromCanister(account) {
4123
4181
  }
4124
4182
  return { type: "External", address: account.External };
4125
4183
  }
4126
- function accountFromWire(account) {
4127
- if ("Native" in account) {
4128
- return { type: "Native", principal: account.Native };
4129
- }
4130
- if ("External" in account) {
4131
- return { type: "External", address: account.External };
4132
- }
4133
- if (account.type === "Native") {
4134
- return { type: "Native", principal: account.principal };
4135
- }
4136
- return { type: "External", address: account.address };
4137
- }
4138
4184
  function mapInstantLoanEvent(event) {
4139
4185
  return {
4140
4186
  id: event.id,
@@ -4253,55 +4299,80 @@ function decodeRef(ref) {
4253
4299
  );
4254
4300
  }
4255
4301
  }
4256
- function mapCandidateWire(wire) {
4257
- const loanId = parseBigintWire(wire.loanId ?? wire.loan_id, "loan ID");
4258
- const ref = wire.ref ?? wire.short_ref ?? publicIdFromInt(loanId);
4259
- const createdAt = wire.createdAt ?? wire.created_at;
4260
- return {
4261
- loanId,
4262
- ref,
4263
- profileId: requiredString(
4264
- wire.profileId ?? wire.lending_profile,
4265
- "profile ID"
4266
- ),
4267
- ...createdAt ? { createdAt: new Date(createdAt) } : {},
4268
- collateralPoolId: requiredString(
4269
- wire.collateralPoolId ?? wire.lend_pool_ic_id,
4270
- "collateral pool ID"
4271
- ),
4272
- borrowPoolId: requiredString(
4273
- wire.borrowPoolId ?? wire.borrow_pool_ic_id,
4274
- "borrow pool ID"
4275
- ),
4276
- collateralAsset: requiredString(
4277
- wire.collateralAsset ?? wire.lend_asset,
4278
- "collateral asset"
4279
- ),
4280
- borrowAsset: requiredString(
4281
- wire.borrowAsset ?? wire.borrow_asset,
4282
- "borrow asset"
4283
- ),
4284
- collateralAmount: parseBigintWire(
4285
- wire.collateralAmount,
4286
- "collateral amount"
4287
- )
4288
- };
4289
- }
4290
- function parseBigintWire(value, label) {
4291
- if (!value || !/^\d+$/.test(value)) {
4302
+ function validateInstantLoanFindQuery(query) {
4303
+ if (typeof query !== "string") {
4292
4304
  throw new LiquidiumError(
4293
4305
  LiquidiumErrorCode.VALIDATION_ERROR,
4294
- `Invalid instant loan ${label}`
4306
+ "Instant loan find query must be a string"
4295
4307
  );
4296
4308
  }
4297
- return BigInt(value);
4309
+ const trimmedQuery = query.trim();
4310
+ if (!trimmedQuery) {
4311
+ throw new LiquidiumError(
4312
+ LiquidiumErrorCode.VALIDATION_ERROR,
4313
+ "Instant loan find query must be non-empty"
4314
+ );
4315
+ }
4316
+ if (trimmedQuery.length > INSTANT_LOAN_FIND_QUERY_MAX_LENGTH) {
4317
+ throw new LiquidiumError(
4318
+ LiquidiumErrorCode.VALIDATION_ERROR,
4319
+ `Instant loan find query must be at most ${INSTANT_LOAN_FIND_QUERY_MAX_LENGTH.toString()} characters`
4320
+ );
4321
+ }
4322
+ return trimmedQuery;
4298
4323
  }
4299
- function requiredString(value, label) {
4300
- if (value?.trim()) return value;
4301
- throw new LiquidiumError(
4302
- LiquidiumErrorCode.VALIDATION_ERROR,
4303
- `Missing instant loan ${label}`
4304
- );
4324
+ function uniqueInstantLoanFindCandidates(candidates) {
4325
+ const candidatesByLoanId = /* @__PURE__ */ new Map();
4326
+ for (const candidate of candidates) {
4327
+ if (!candidatesByLoanId.has(candidate.loanId)) {
4328
+ candidatesByLoanId.set(candidate.loanId, candidate);
4329
+ }
4330
+ }
4331
+ return [...candidatesByLoanId.values()];
4332
+ }
4333
+ function mapCandidateWire(wire) {
4334
+ return {
4335
+ loanId: parseUnsignedApiBigint(wire.loan_id, {
4336
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4337
+ label: "loan ID"
4338
+ }),
4339
+ ref: parseNonEmptyApiString(wire.short_ref, {
4340
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4341
+ label: "short reference"
4342
+ }),
4343
+ createdAt: parseIsoApiTimestampToUnixSeconds(wire.created_at, {
4344
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4345
+ label: "creation timestamp"
4346
+ }),
4347
+ collateral: {
4348
+ poolId: parseNonEmptyApiString(wire.lend_pool_ic_id, {
4349
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4350
+ label: "lend pool ID"
4351
+ }),
4352
+ asset: parseApiStringUnion(wire.lend_asset, INSTANT_LOAN_ASSETS, {
4353
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4354
+ label: "lend asset"
4355
+ }),
4356
+ amount: parseUnsignedApiBigint(wire.collateral_amount, {
4357
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4358
+ label: "collateral amount"
4359
+ })
4360
+ },
4361
+ borrow: {
4362
+ poolId: parseNonEmptyApiString(wire.borrow_pool_ic_id, {
4363
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4364
+ label: "borrow pool ID"
4365
+ }),
4366
+ asset: parseApiStringUnion(wire.borrow_asset, INSTANT_LOAN_ASSETS, {
4367
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4368
+ label: "borrow asset"
4369
+ })
4370
+ },
4371
+ profileId: parseNonEmptyApiString(wire.profile, {
4372
+ context: INSTANT_LOAN_WIRE_CONTEXT,
4373
+ label: "profile ID"
4374
+ })
4375
+ };
4305
4376
  }
4306
4377
  function mapInstantLoansErrorToLiquidiumError(error) {
4307
4378
  const [key, payload] = Object.entries(error)[0];
@@ -5906,7 +5977,7 @@ var PositionsModule = class {
5906
5977
  pool,
5907
5978
  priceUsd,
5908
5979
  suppliedUsd: nativeAmountToUsdScaled(
5909
- position.deposited + position.earnedInterest,
5980
+ position.deposited,
5910
5981
  position.depositedDecimals,
5911
5982
  priceUsd
5912
5983
  ),
@@ -5941,6 +6012,24 @@ var PositionsModule = class {
5941
6012
  const buffered = rawDebt * (BPS_SCALE + bufferBps) / BPS_SCALE;
5942
6013
  return { amount: buffered, decimals: position.borrowedDecimals };
5943
6014
  }
6015
+ /**
6016
+ * Returns the current full withdraw amount for a position.
6017
+ *
6018
+ * `Position.deposited` already reflects the current supplied balance at the
6019
+ * latest lending index; do not add `earnedInterest` to this amount.
6020
+ * Pass `amount` to withdraw calls and use `decimals` for display formatting.
6021
+ *
6022
+ * @param profileId - The Liquidium profile principal text.
6023
+ * @param poolId - The pool principal text.
6024
+ * @returns Full withdraw amount in the supplied asset's base units.
6025
+ */
6026
+ async getFullWithdrawAmount(profileId, poolId) {
6027
+ const position = await this.getPosition(profileId, poolId);
6028
+ if (!position) {
6029
+ return { amount: 0n, decimals: 0n };
6030
+ }
6031
+ return { amount: position.deposited, decimals: position.depositedDecimals };
6032
+ }
5944
6033
  };
5945
6034
  function nativeAmountToUsdScaled(amount, nativeDecimals, priceUsd) {
5946
6035
  if (amount <= 0n || priceUsd <= 0) {