@liquidium/client 0.3.4 → 0.4.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.cjs CHANGED
@@ -32,11 +32,7 @@ var SupplyAction = {
32
32
  var OutflowType = {
33
33
  borrow: "borrow",
34
34
  feeClaim: "feeClaim",
35
- withdraw: "withdraw"
36
- };
37
- var InflowSubmitType = {
38
- DEPOSIT: "DEPOSIT",
39
- REPAY: "REPAY"
35
+ withdrawal: "withdrawal"
40
36
  };
41
37
 
42
38
  // src/core/config.ts
@@ -1370,8 +1366,8 @@ function mapCanisterWalletToWallet(canisterWallet) {
1370
1366
  );
1371
1367
  }
1372
1368
  }
1373
- var KNOWN_ASSET_TAGS = ["BTC", "SOL", "USDC", "USDT"];
1374
- var KNOWN_CHAIN_TAGS = ["BTC", "ETH", "SOL"];
1369
+ var KNOWN_ASSET_TAGS = ["BTC", "USDC", "USDT"];
1370
+ var KNOWN_CHAIN_TAGS = ["BTC", "ETH"];
1375
1371
  function extractVariantTag(variant, knownTags) {
1376
1372
  const [key] = Object.keys(variant);
1377
1373
  if (!key) {
@@ -1684,49 +1680,24 @@ function decodeLoanCreatedEvent(payload) {
1684
1680
  }
1685
1681
 
1686
1682
  // src/core/sdk-api-paths.ts
1687
- var SDK_API_VERSION = {
1688
- activities: "v1",
1689
- history: "v1",
1690
- inflow: "v1",
1691
- instantLoans: "v1"
1692
- };
1683
+ var SDK_API_V1_PATH = "/v1";
1684
+ var SDK_API_V2_PATH = "/v2";
1693
1685
  var SdkApiQueryParam = {
1694
1686
  cursor: "cursor",
1695
1687
  from: "from",
1688
+ filter: "filter",
1696
1689
  limit: "limit",
1697
1690
  market: "market",
1691
+ operations: "operations",
1698
1692
  poolId: "poolId",
1699
1693
  profileId: "profileId",
1700
- state: "state",
1701
- statuses: "statuses",
1702
- to: "to",
1703
- types: "types"
1694
+ states: "states",
1695
+ to: "to"
1704
1696
  };
1705
- var ACTIVITIES = `/${SDK_API_VERSION.activities}/activities`;
1706
- var HISTORY_POOL = `/${SDK_API_VERSION.history}/history/pool`;
1707
- var HISTORY_POOL_CONFIG = `/${SDK_API_VERSION.history}/history/pool-config`;
1708
- var HISTORY_RATES = `/${SDK_API_VERSION.history}/history/rates`;
1709
- var HISTORY_USERS = `/${SDK_API_VERSION.history}/history/users`;
1710
- var INFLOW = `/${SDK_API_VERSION.inflow}/inflow`;
1711
- var INSTANT_LOANS = `/${SDK_API_VERSION.instantLoans}/instant-loans`;
1712
- function buildHistoryPoolPath(poolId, query) {
1713
- const base = `${HISTORY_POOL}/${encodeURIComponent(poolId)}`;
1714
- const qs = query.toString();
1715
- return qs ? `${base}?${qs}` : base;
1716
- }
1717
- function buildHistoryPoolConfigPath(poolId, cursor) {
1718
- const base = `${HISTORY_POOL_CONFIG}/${encodeURIComponent(poolId)}`;
1719
- if (!cursor) {
1720
- return base;
1721
- }
1722
- const query = new URLSearchParams({ [SdkApiQueryParam.cursor]: cursor });
1723
- return `${base}?${query.toString()}`;
1724
- }
1725
- function buildHistoryRatesPath(poolId, query) {
1726
- const base = `${HISTORY_RATES}/${encodeURIComponent(poolId)}`;
1727
- const qs = query.toString();
1728
- return qs ? `${base}?${qs}` : base;
1729
- }
1697
+ var ACTIVITIES = `${SDK_API_V2_PATH}/activities`;
1698
+ var HISTORY_USERS = `${SDK_API_V2_PATH}/history/users`;
1699
+ var INFLOW = `${SDK_API_V2_PATH}/inflow`;
1700
+ var INSTANT_LOANS = `${SDK_API_V1_PATH}/instant-loans`;
1730
1701
  function buildHistoryUserTransactionsPath(user, query) {
1731
1702
  const base = `${HISTORY_USERS}/${encodeURIComponent(user)}/transactions`;
1732
1703
  const qs = query.toString();
@@ -1741,8 +1712,8 @@ function buildActivitiesPath(request) {
1741
1712
  const query = new URLSearchParams({
1742
1713
  [SdkApiQueryParam.profileId]: request.profileId
1743
1714
  });
1744
- if (request.state) {
1745
- query.set(SdkApiQueryParam.state, request.state);
1715
+ if (request.filter) {
1716
+ query.set(SdkApiQueryParam.filter, request.filter);
1746
1717
  }
1747
1718
  return `${ACTIVITIES}?${query.toString()}`;
1748
1719
  }
@@ -1800,12 +1771,6 @@ function parseBigInt(value, label) {
1800
1771
  );
1801
1772
  }
1802
1773
  }
1803
- function parseOptionalBigInt(value, label) {
1804
- if (value === void 0) {
1805
- return void 0;
1806
- }
1807
- return parseBigInt(value, label);
1808
- }
1809
1774
 
1810
1775
  // src/modules/instant-loans/ref-code.ts
1811
1776
  var REF_LENGTH = 6;
@@ -1876,28 +1841,8 @@ var ActivityFilter = {
1876
1841
  completed: "completed",
1877
1842
  all: "all"
1878
1843
  };
1879
- var ActivityDirection = {
1880
- inflow: "inflow",
1881
- outflow: "outflow"
1882
- };
1883
- var ActivityKind = {
1884
- deposit: "deposit",
1885
- repayment: "repayment",
1886
- borrow: "borrow",
1887
- withdraw: "withdraw"
1888
- };
1889
- var ActivityStatus = {
1890
- requested: "requested",
1891
- pending: "pending",
1892
- detected: "detected",
1893
- processing: "processing",
1894
- sent: "sent",
1895
- confirmed: "confirmed",
1896
- failed: "failed"
1897
- };
1898
1844
 
1899
1845
  // src/modules/activities/activities.ts
1900
- var PRE_TERMINAL_ETH_ACTIVITY_ID_PREFIX = "pre_terminal_eth_";
1901
1846
  var ActivitiesModule = class {
1902
1847
  constructor(apiClient, canisterContext) {
1903
1848
  this.apiClient = apiClient;
@@ -1906,11 +1851,11 @@ var ActivitiesModule = class {
1906
1851
  apiClient;
1907
1852
  canisterContext;
1908
1853
  /**
1909
- * Lists profile activities. Defaults to all activities.
1854
+ * Lists profile activities. Defaults to active activities.
1910
1855
  *
1911
1856
  * Uses the Liquidium SDK API.
1912
1857
  *
1913
- * @param request - Profile id or instant-loan short reference plus optional state filter.
1858
+ * @param request - Profile id or instant-loan short reference plus optional lifecycle filter.
1914
1859
  * @returns Activities owned by the resolved profile.
1915
1860
  */
1916
1861
  async list(request) {
@@ -1919,7 +1864,7 @@ var ActivitiesModule = class {
1919
1864
  const response = await apiClient.get(
1920
1865
  buildActivitiesPath({
1921
1866
  profileId,
1922
- state: request.filter ?? ActivityFilter.all
1867
+ filter: request.filter ?? ActivityFilter.active
1923
1868
  })
1924
1869
  );
1925
1870
  return response.activities.map(mapActivity);
@@ -2000,76 +1945,48 @@ function mapInstantLoanLookupError(error) {
2000
1945
  return new LiquidiumError(LiquidiumErrorCode.INTERNAL, key);
2001
1946
  }
2002
1947
  function mapActivity(wire) {
2003
- const { amount, stage, status, topUp: topUpWire, ...activity } = wire;
2004
- const effectiveStage = stage ?? deriveActivityStage(wire);
2005
- const topUp = activity.direction === ActivityDirection.inflow && topUpWire ? mapActivityTopUp(topUpWire) : void 0;
2006
- if (activity.direction === ActivityDirection.inflow) {
2007
- return {
2008
- ...activity,
2009
- direction: ActivityDirection.inflow,
2010
- kind: mapInflowActivityKind(activity.kind),
2011
- status: mapInflowActivityStatus(
2012
- mapActivityStatus(status, effectiveStage)
2013
- ),
2014
- ...topUp ? { topUp } : {},
2015
- amount: parseBigInt(amount, "activity amount")
2016
- };
2017
- }
2018
- return {
2019
- ...activity,
2020
- direction: ActivityDirection.outflow,
2021
- kind: mapOutflowActivityKind(activity.kind),
2022
- status: mapOutflowActivityStatus(mapActivityStatus(status, effectiveStage)),
2023
- amount: parseBigInt(amount, "activity amount")
1948
+ const amount = parseBigInt(wire.amount, "activity amount");
1949
+ const activityBase = {
1950
+ id: wire.id,
1951
+ poolId: wire.poolId,
1952
+ asset: wire.asset,
1953
+ chain: wire.chain,
1954
+ amount,
1955
+ timestampMs: wire.timestampMs
2024
1956
  };
2025
- }
2026
- function mapInflowActivityKind(kind) {
2027
- if (kind === "deposit" || kind === "repayment") {
2028
- return kind;
2029
- }
2030
- throw new LiquidiumError(
2031
- LiquidiumErrorCode.INTERNAL,
2032
- `Invalid inflow activity kind: ${kind}`
2033
- );
2034
- }
2035
- function mapOutflowActivityKind(kind) {
2036
- if (kind === "borrow" || kind === "withdraw") {
2037
- return kind;
1957
+ if (isInflowOperation(wire.status.operation)) {
1958
+ const activity = {
1959
+ ...activityBase,
1960
+ status: wire.status
1961
+ };
1962
+ if (wire.txids) {
1963
+ activity.txids = wire.txids;
1964
+ }
1965
+ if (wire.topUp) {
1966
+ activity.topUp = mapActivityTopUp(wire.topUp);
1967
+ }
1968
+ return activity;
2038
1969
  }
2039
- throw new LiquidiumError(
2040
- LiquidiumErrorCode.INTERNAL,
2041
- `Invalid outflow activity kind: ${kind}`
2042
- );
2043
- }
2044
- function mapInflowActivityStatus(status) {
2045
- if (status !== "sent") {
2046
- return status;
1970
+ if (isOutflowOperation(wire.status.operation)) {
1971
+ const activity = {
1972
+ ...activityBase,
1973
+ status: wire.status
1974
+ };
1975
+ if (wire.txids) {
1976
+ activity.txids = wire.txids;
1977
+ }
1978
+ return activity;
2047
1979
  }
2048
1980
  throw new LiquidiumError(
2049
1981
  LiquidiumErrorCode.INTERNAL,
2050
- `Invalid inflow activity status: ${status}`
1982
+ `Invalid activity operation: ${wire.status.operation}`
2051
1983
  );
2052
1984
  }
2053
- function mapOutflowActivityStatus(status) {
2054
- if (status !== "detected" && status !== "processing") {
2055
- return status;
2056
- }
2057
- throw new LiquidiumError(
2058
- LiquidiumErrorCode.INTERNAL,
2059
- `Invalid outflow activity status: ${status}`
2060
- );
1985
+ function isInflowOperation(operation) {
1986
+ return operation === "deposit" || operation === "repayment";
2061
1987
  }
2062
- function mapActivityStatus(status, stage) {
2063
- if (status !== "pending") {
2064
- return status;
2065
- }
2066
- if (stage === "deposited") {
2067
- return "detected";
2068
- }
2069
- if (stage === "confirmed" || stage === "finalising" || stage === "pending") {
2070
- return "processing";
2071
- }
2072
- return "pending";
1988
+ function isOutflowOperation(operation) {
1989
+ return operation === "borrow" || operation === "withdrawal";
2073
1990
  }
2074
1991
  function mapActivityTopUp(wire) {
2075
1992
  return {
@@ -2085,24 +2002,6 @@ function mapActivityTopUp(wire) {
2085
2002
  )
2086
2003
  };
2087
2004
  }
2088
- function deriveActivityStage(wire) {
2089
- if (wire.id.startsWith(PRE_TERMINAL_ETH_ACTIVITY_ID_PREFIX) && wire.direction === ActivityDirection.inflow && wire.chain === Chain.ETH) {
2090
- return "deposited";
2091
- }
2092
- return void 0;
2093
- }
2094
-
2095
- // src/core/rates.ts
2096
- var RATE_SCALE = 1000000000000000000000000000n;
2097
- var RATE_DECIMALS = BigInt(RATE_SCALE.toString().length - 1);
2098
-
2099
- // src/modules/history/types.ts
2100
- var UserHistoryStatus = {
2101
- requested: "requested",
2102
- pending: "pending",
2103
- confirmed: "confirmed",
2104
- failed: "failed"
2105
- };
2106
2005
 
2107
2006
  // src/modules/history/history.ts
2108
2007
  var HistoryModule = class {
@@ -2120,154 +2019,41 @@ var HistoryModule = class {
2120
2019
  return this.apiClient;
2121
2020
  }
2122
2021
  /**
2123
- * Returns paginated rate and utilization history for a pool.
2124
- *
2125
- * @param poolId - The pool principal text.
2126
- * @param window - Optional time window with from/to timestamps and limit.
2127
- * @returns A page of pool rate history entries and the next cursor when more results are available.
2128
- */
2129
- async getPoolHistory(poolId, window = {}) {
2130
- const apiClient = this.requireApi();
2131
- const query = createHistoryWindowQuery(window);
2132
- const requestPath = buildHistoryPoolPath(poolId, query);
2133
- const response = await apiClient.get(requestPath);
2134
- return {
2135
- items: response.items.map((item) => ({
2136
- date: item.date,
2137
- rateDecimals: RATE_DECIMALS,
2138
- avgBorrowRate: parseBigInt(item.avgBorrowRate, "pool borrow rate"),
2139
- avgLendRate: parseBigInt(item.avgLendRate, "pool lend rate"),
2140
- avgUtilizationRate: parseBigInt(
2141
- item.avgUtilizationRate,
2142
- "pool utilization rate"
2143
- )
2144
- })),
2145
- nextCursor: response.nextCursor
2146
- };
2147
- }
2148
- /**
2149
- * Returns paginated configuration change history for a pool.
2150
- *
2151
- * @param poolId - The pool principal text.
2152
- * @param cursor - An optional pagination cursor from a previous response.
2153
- * @returns A page of pool configuration changes and the next cursor when more results are available.
2154
- */
2155
- async getPoolConfigHistory(poolId, cursor) {
2156
- const apiClient = this.requireApi();
2157
- const requestPath = buildHistoryPoolConfigPath(poolId, cursor);
2158
- const response = await apiClient.get(requestPath);
2159
- return {
2160
- items: response.items.map((item) => ({
2161
- type: item.type,
2162
- poolId: item.poolId,
2163
- asset: item.asset,
2164
- chain: item.chain,
2165
- timestamp: item.timestamp,
2166
- totalSupply: parseBigInt(item.totalSupply, "pool history totalSupply"),
2167
- totalDebt: parseBigInt(item.totalDebt, "pool history totalDebt"),
2168
- supplyCap: parseOptionalBigInt(
2169
- item.supplyCap,
2170
- "pool history supplyCap"
2171
- ),
2172
- borrowCap: parseOptionalBigInt(
2173
- item.borrowCap,
2174
- "pool history borrowCap"
2175
- ),
2176
- maxLtv: parseBigInt(item.maxLtv, "pool history maxLtv"),
2177
- liquidationThreshold: parseBigInt(
2178
- item.liquidationThreshold,
2179
- "pool history liquidationThreshold"
2180
- ),
2181
- liquidationBonus: parseBigInt(
2182
- item.liquidationBonus,
2183
- "pool history liquidationBonus"
2184
- ),
2185
- protocolLiquidationFee: parseBigInt(
2186
- item.protocolLiquidationFee,
2187
- "pool history protocolLiquidationFee"
2188
- ),
2189
- reserveFactor: parseBigInt(
2190
- item.reserveFactor,
2191
- "pool history reserveFactor"
2192
- ),
2193
- baseRate: parseBigInt(item.baseRate, "pool history baseRate"),
2194
- optimalUtilizationRate: parseBigInt(
2195
- item.optimalUtilizationRate,
2196
- "pool history optimalUtilizationRate"
2197
- ),
2198
- rateSlopeBefore: parseBigInt(
2199
- item.rateSlopeBefore,
2200
- "pool history rateSlopeBefore"
2201
- ),
2202
- rateSlopeAfter: parseBigInt(
2203
- item.rateSlopeAfter,
2204
- "pool history rateSlopeAfter"
2205
- ),
2206
- lendingIndex: parseBigInt(
2207
- item.lendingIndex,
2208
- "pool history lendingIndex"
2209
- ),
2210
- borrowIndex: parseBigInt(item.borrowIndex, "pool history borrowIndex"),
2211
- sameAssetBorrowing: item.sameAssetBorrowing,
2212
- frozen: item.frozen
2213
- })),
2214
- nextCursor: response.nextCursor
2215
- };
2216
- }
2217
- /**
2218
- * Returns borrow rate history for a pool.
2022
+ * Returns transaction history for a user.
2219
2023
  *
2220
- * @param poolId - The pool principal text.
2221
- * @param window - Optional time window with from/to timestamps and limit.
2222
- * @returns Paginated APY samples.
2024
+ * @param user - The Liquidium profile principal text.
2025
+ * @param filters - Optional pool, operation, state, time range, and pagination filters.
2026
+ * @returns Paginated user history entries.
2223
2027
  */
2224
- async getBorrowRateHistory(poolId, window = {}) {
2225
- const apiClient = this.requireApi();
2226
- const query = createHistoryWindowQuery(window);
2227
- const requestPath = buildHistoryRatesPath(poolId, query);
2228
- const response = await apiClient.get(requestPath);
2229
- return {
2230
- items: response.items.map((item) => ({
2231
- date: item.date,
2232
- rateDecimals: RATE_DECIMALS,
2233
- avgRate: parseBigInt(item.avgRate, "borrow rate")
2234
- })),
2235
- nextCursor: response.nextCursor
2236
- };
2237
- }
2238
- async getUserTransactionHistory(user, marketOrFilters, filters = {}) {
2028
+ async getUserTransactionHistory(user, filters = {}) {
2239
2029
  const apiClient = this.requireApi();
2240
- const normalizedFilters = normalizeTransactionHistoryFilters(
2241
- marketOrFilters,
2242
- filters
2243
- );
2244
2030
  const query = new URLSearchParams();
2245
- if (normalizedFilters.cursor) {
2246
- query.set(SdkApiQueryParam.cursor, normalizedFilters.cursor);
2031
+ if (filters.cursor) {
2032
+ query.set(SdkApiQueryParam.cursor, filters.cursor);
2247
2033
  }
2248
- if (normalizedFilters.market) {
2249
- query.set(SdkApiQueryParam.market, normalizedFilters.market);
2034
+ if (filters.market) {
2035
+ query.set(SdkApiQueryParam.market, filters.market);
2250
2036
  }
2251
- if (normalizedFilters.poolId) {
2252
- query.set(SdkApiQueryParam.poolId, normalizedFilters.poolId);
2037
+ if (filters.poolId) {
2038
+ query.set(SdkApiQueryParam.poolId, filters.poolId);
2253
2039
  }
2254
- if (normalizedFilters.types?.length) {
2255
- query.set(SdkApiQueryParam.types, normalizedFilters.types.join(","));
2040
+ if (filters.operations?.length) {
2041
+ query.set(SdkApiQueryParam.operations, filters.operations.join(","));
2256
2042
  }
2257
- if (normalizedFilters.statuses?.length) {
2043
+ if (filters.states?.length) {
2258
2044
  query.set(
2259
- SdkApiQueryParam.statuses,
2260
- normalizedFilters.statuses.map(mapHistoryStatusToApi).join(",")
2045
+ SdkApiQueryParam.states,
2046
+ createHistoryStateFilterParam(filters.states)
2261
2047
  );
2262
2048
  }
2263
- if (normalizedFilters.from) {
2264
- query.set(SdkApiQueryParam.from, normalizedFilters.from);
2049
+ if (filters.from) {
2050
+ query.set(SdkApiQueryParam.from, filters.from);
2265
2051
  }
2266
- if (normalizedFilters.to) {
2267
- query.set(SdkApiQueryParam.to, normalizedFilters.to);
2052
+ if (filters.to) {
2053
+ query.set(SdkApiQueryParam.to, filters.to);
2268
2054
  }
2269
- if (normalizedFilters.limit !== void 0) {
2270
- query.set(SdkApiQueryParam.limit, String(normalizedFilters.limit));
2055
+ if (filters.limit !== void 0) {
2056
+ query.set(SdkApiQueryParam.limit, String(filters.limit));
2271
2057
  }
2272
2058
  const requestPath = buildHistoryUserTransactionsPath(user, query);
2273
2059
  const response = await apiClient.get(requestPath);
@@ -2276,30 +2062,33 @@ var HistoryModule = class {
2276
2062
  nextCursor: response.nextCursor
2277
2063
  };
2278
2064
  }
2279
- async getLiquidationHistory(user, marketOrFilters, filters = {}) {
2065
+ /**
2066
+ * Returns liquidation history for a user.
2067
+ *
2068
+ * @param user - The Liquidium profile principal text.
2069
+ * @param filters - Optional pool, time range, and pagination filters.
2070
+ * @returns Paginated liquidation history entries.
2071
+ */
2072
+ async getLiquidationHistory(user, filters = {}) {
2280
2073
  const apiClient = this.requireApi();
2281
- const normalizedFilters = normalizeLiquidationHistoryFilters(
2282
- marketOrFilters,
2283
- filters
2284
- );
2285
2074
  const query = new URLSearchParams();
2286
- if (normalizedFilters.cursor) {
2287
- query.set(SdkApiQueryParam.cursor, normalizedFilters.cursor);
2075
+ if (filters.cursor) {
2076
+ query.set(SdkApiQueryParam.cursor, filters.cursor);
2288
2077
  }
2289
- if (normalizedFilters.market) {
2290
- query.set(SdkApiQueryParam.market, normalizedFilters.market);
2078
+ if (filters.market) {
2079
+ query.set(SdkApiQueryParam.market, filters.market);
2291
2080
  }
2292
- if (normalizedFilters.poolId) {
2293
- query.set(SdkApiQueryParam.poolId, normalizedFilters.poolId);
2081
+ if (filters.poolId) {
2082
+ query.set(SdkApiQueryParam.poolId, filters.poolId);
2294
2083
  }
2295
- if (normalizedFilters.from) {
2296
- query.set(SdkApiQueryParam.from, normalizedFilters.from);
2084
+ if (filters.from) {
2085
+ query.set(SdkApiQueryParam.from, filters.from);
2297
2086
  }
2298
- if (normalizedFilters.to) {
2299
- query.set(SdkApiQueryParam.to, normalizedFilters.to);
2087
+ if (filters.to) {
2088
+ query.set(SdkApiQueryParam.to, filters.to);
2300
2089
  }
2301
- if (normalizedFilters.limit !== void 0) {
2302
- query.set(SdkApiQueryParam.limit, String(normalizedFilters.limit));
2090
+ if (filters.limit !== void 0) {
2091
+ query.set(SdkApiQueryParam.limit, String(filters.limit));
2303
2092
  }
2304
2093
  const requestPath = buildHistoryUserLiquidationsPath(user, query);
2305
2094
  const response = await apiClient.get(requestPath);
@@ -2312,77 +2101,59 @@ var HistoryModule = class {
2312
2101
  function mapUserTransactionHistoryEntry(item) {
2313
2102
  return {
2314
2103
  id: item.id,
2315
- type: mapUserTransactionHistoryType(item.type),
2316
2104
  amount: parseBigInt(item.amount, "history user amount"),
2317
2105
  poolId: item.poolId,
2318
2106
  timestamp: item.timestamp,
2319
- status: mapHistoryStatusFromApi(item.status),
2107
+ status: item.status,
2320
2108
  txids: item.txids
2321
2109
  };
2322
2110
  }
2323
2111
  function mapUserLiquidationHistoryEntry(item) {
2324
- if (item.type !== "liquidation") {
2112
+ if (item.status.operation !== "liquidation" || item.status.state !== "completed") {
2325
2113
  throw new LiquidiumError(
2326
2114
  LiquidiumErrorCode.INTERNAL,
2327
- `Invalid liquidation history type: ${item.type}`
2115
+ `Invalid liquidation history status: ${item.status.state}`
2328
2116
  );
2329
2117
  }
2330
2118
  return {
2331
2119
  id: item.id,
2332
- type: "liquidation",
2333
2120
  amount: parseBigInt(item.amount, "history user amount"),
2334
2121
  poolId: item.poolId,
2335
2122
  timestamp: item.timestamp,
2336
- status: mapLiquidationHistoryStatus(item.status),
2123
+ status: item.status,
2337
2124
  txids: item.txids
2338
2125
  };
2339
2126
  }
2340
- function mapUserTransactionHistoryType(type) {
2341
- if (type !== "liquidation") {
2342
- return type;
2343
- }
2344
- throw new LiquidiumError(
2345
- LiquidiumErrorCode.INTERNAL,
2346
- "Transaction history response included a liquidation entry"
2347
- );
2348
- }
2349
- function mapLiquidationHistoryStatus(status) {
2350
- const mappedStatus = mapHistoryStatusFromApi(status);
2351
- if (mappedStatus === UserHistoryStatus.confirmed) {
2352
- return mappedStatus;
2353
- }
2354
- throw new LiquidiumError(
2355
- LiquidiumErrorCode.INTERNAL,
2356
- `Invalid liquidation history status: ${status}`
2357
- );
2358
- }
2359
- function createHistoryWindowQuery(window) {
2360
- const query = new URLSearchParams();
2361
- if (window.cursor) query.set(SdkApiQueryParam.cursor, window.cursor);
2362
- if (window.from) query.set(SdkApiQueryParam.from, window.from);
2363
- if (window.to) query.set(SdkApiQueryParam.to, window.to);
2364
- if (window.limit !== void 0) {
2365
- query.set(SdkApiQueryParam.limit, String(window.limit));
2127
+ function createHistoryStateFilterParam(states) {
2128
+ for (const state of states) {
2129
+ validateHistoryStateFilter(state);
2366
2130
  }
2367
- return query;
2131
+ return [...new Set(states)].join(",");
2368
2132
  }
2369
- function normalizeTransactionHistoryFilters(marketOrFilters, filters) {
2370
- if (typeof marketOrFilters === "string") {
2371
- return { ...filters, market: marketOrFilters };
2372
- }
2373
- return { ...marketOrFilters ?? {}, ...filters };
2374
- }
2375
- function normalizeLiquidationHistoryFilters(marketOrFilters, filters) {
2376
- if (typeof marketOrFilters === "string") {
2377
- return { ...filters, market: marketOrFilters };
2133
+ function validateHistoryStateFilter(state) {
2134
+ switch (state) {
2135
+ case "action_required":
2136
+ case "confirming":
2137
+ case "processing":
2138
+ case "completed":
2139
+ case "failed":
2140
+ return;
2141
+ default:
2142
+ throw new LiquidiumError(
2143
+ LiquidiumErrorCode.VALIDATION_ERROR,
2144
+ `History state filter is not supported: ${state}`
2145
+ );
2378
2146
  }
2379
- return { ...marketOrFilters ?? {}, ...filters };
2380
2147
  }
2381
- function mapHistoryStatusFromApi(status) {
2382
- return status.toLowerCase();
2383
- }
2384
- function mapHistoryStatusToApi(status) {
2385
- return status.toUpperCase();
2148
+
2149
+ // src/core/status.ts
2150
+ function createLiquidiumStatus(params) {
2151
+ return {
2152
+ operation: params.operation,
2153
+ state: params.state,
2154
+ confirmations: params.confirmations ?? null,
2155
+ requiredConfirmations: params.requiredConfirmations ?? null
2156
+ };
2386
2157
  }
2387
2158
 
2388
2159
  // src/core/utils/api-response-parsers.ts
@@ -3979,20 +3750,11 @@ function getChainForInstantLoanAsset(asset) {
3979
3750
  return asset;
3980
3751
  }
3981
3752
 
3982
- // src/modules/instant-loans/types.ts
3983
- var InstantLoanStatus = {
3984
- awaitingDeposit: "awaiting_deposit",
3985
- depositDetected: "deposit_detected",
3986
- active: "active",
3987
- settling: "settling",
3988
- closed: "closed",
3989
- expired: "expired"
3990
- };
3991
-
3992
3753
  // src/modules/instant-loans/instant-loans.ts
3993
3754
  var REPAYMENT_BUFFER_SECONDS = 86400n;
3994
- var RATE_SCALE2 = 10n ** 27n;
3755
+ var RATE_SCALE = 10n ** 27n;
3995
3756
  var SECONDS_PER_YEAR = 31536000n;
3757
+ var MILLISECONDS_PER_SECOND3 = 1e3;
3996
3758
  var ETH_STABLECOIN_INFLOW_FEE_FALLBACK = 1500000n;
3997
3759
  var INSTANT_LOAN_MIN_SLIPPAGE_BUFFER_BPS = 200n;
3998
3760
  var INSTANT_LOAN_FIND_QUERY_MAX_LENGTH = 256;
@@ -4004,14 +3766,16 @@ var INSTANT_LOAN_ASSETS = [
4004
3766
  Asset.USDT
4005
3767
  ];
4006
3768
  var InstantLoansModule = class {
4007
- constructor(canisterContext, apiClient, lending, positions) {
3769
+ constructor(canisterContext, apiClient, activities, lending, positions) {
4008
3770
  this.canisterContext = canisterContext;
4009
3771
  this.apiClient = apiClient;
3772
+ this.activities = activities;
4010
3773
  this.lending = lending;
4011
3774
  this.positions = positions;
4012
3775
  }
4013
3776
  canisterContext;
4014
3777
  apiClient;
3778
+ activities;
4015
3779
  lending;
4016
3780
  positions;
4017
3781
  /**
@@ -4211,7 +3975,8 @@ var InstantLoansModule = class {
4211
3975
  const response = await apiClient.get(
4212
3976
  buildInstantLoanFindPath({ query })
4213
3977
  );
4214
- return (response.candidates ?? response.loans ?? []).map(mapCandidateWire);
3978
+ const candidates = "candidates" in response ? response.candidates : response.loans;
3979
+ return candidates.map(mapCandidateWire);
4215
3980
  }
4216
3981
  async getLoanRecord(loanId) {
4217
3982
  try {
@@ -4250,6 +4015,7 @@ var InstantLoansModule = class {
4250
4015
  borrowAmount: record.borrow_amount,
4251
4016
  borrowDestination: accountFromCanister(record.borrow_destination),
4252
4017
  refundDestination: accountFromCanister(record.refund_destination),
4018
+ started: record.started,
4253
4019
  depositDetectedTimestamp: record.deposit_detected_ts[0] ?? null,
4254
4020
  expiryTimestamp: record.expires_at[0] ?? deriveDepositExpiryTimestamp({
4255
4021
  depositDetectedTimestamp: record.deposit_detected_ts[0] ?? null,
@@ -4278,7 +4044,8 @@ var InstantLoansModule = class {
4278
4044
  repayTarget,
4279
4045
  collateralPosition,
4280
4046
  borrowPosition,
4281
- borrowPoolRate
4047
+ borrowPoolRate,
4048
+ activeActivities
4282
4049
  ] = await Promise.all([
4283
4050
  resolveSupplyTarget(this.canisterContext, {
4284
4051
  profileId,
@@ -4294,7 +4061,8 @@ var InstantLoansModule = class {
4294
4061
  }),
4295
4062
  this.positions.getPosition(profileId, collateralPoolId),
4296
4063
  this.positions.getPosition(profileId, borrowPoolId),
4297
- this.positions.market.getPoolRate(borrowPoolId)
4064
+ this.positions.market.getPoolRate(borrowPoolId),
4065
+ this.activities.list({ profileId, filter: ActivityFilter.active })
4298
4066
  ]);
4299
4067
  const totalDebtAmount = calculateTotalDebtAmount(borrowPosition);
4300
4068
  const interestBufferAmount = calculateInterestBufferAmount(
@@ -4311,8 +4079,12 @@ var InstantLoansModule = class {
4311
4079
  const borrowedDecimals = borrowPosition?.borrowedDecimals ?? getAssetNativeDecimals(borrowAsset);
4312
4080
  const debtInterestAmount = borrowPosition?.debtInterest ?? 0n;
4313
4081
  const status = deriveInstantLoanStatus({
4082
+ started: input.started,
4083
+ depositDetectedTimestamp: input.depositDetectedTimestamp,
4084
+ expiryTimestamp: input.expiryTimestamp,
4314
4085
  collateralAmount: currentCollateralAmount,
4315
- totalDebtAmount
4086
+ totalDebtAmount,
4087
+ activeActivities
4316
4088
  });
4317
4089
  const initialDeposit = await this.createInitialDepositQuote({
4318
4090
  collateralAmount,
@@ -4464,7 +4236,7 @@ function calculateInterestBufferAmount(borrowPosition, annualBorrowRate) {
4464
4236
  if (totalDebtAmount <= 0n || annualBorrowRate <= 0n) {
4465
4237
  return 0n;
4466
4238
  }
4467
- const interestBuffer = totalDebtAmount * annualBorrowRate * REPAYMENT_BUFFER_SECONDS / RATE_SCALE2 / SECONDS_PER_YEAR;
4239
+ const interestBuffer = totalDebtAmount * annualBorrowRate * REPAYMENT_BUFFER_SECONDS / RATE_SCALE / SECONDS_PER_YEAR;
4468
4240
  return interestBuffer > 1n ? interestBuffer : 1n;
4469
4241
  }
4470
4242
  function calculateTotalDebtAmount(borrowPosition) {
@@ -4474,13 +4246,75 @@ function calculateTotalDebtAmount(borrowPosition) {
4474
4246
  return borrowPosition.borrowed + borrowPosition.debtInterest;
4475
4247
  }
4476
4248
  function deriveInstantLoanStatus(input) {
4249
+ const activeActivityStatus = deriveActiveInstantLoanActivityStatus(input);
4250
+ if (activeActivityStatus) {
4251
+ return activeActivityStatus;
4252
+ }
4477
4253
  if (input.totalDebtAmount > 0n) {
4478
- return InstantLoanStatus.active;
4254
+ return createLiquidiumStatus({
4255
+ operation: "repayment",
4256
+ state: "active"
4257
+ });
4258
+ }
4259
+ if (input.started) {
4260
+ return createLiquidiumStatus({
4261
+ operation: "repayment",
4262
+ state: "completed"
4263
+ });
4264
+ }
4265
+ if (isInstantLoanDepositExpired(input)) {
4266
+ return createLiquidiumStatus({
4267
+ operation: "deposit",
4268
+ state: "expired"
4269
+ });
4479
4270
  }
4480
4271
  if (input.collateralAmount > 0n) {
4481
- return InstantLoanStatus.depositDetected;
4272
+ return createLiquidiumStatus({
4273
+ operation: "deposit",
4274
+ state: "processing"
4275
+ });
4276
+ }
4277
+ if (input.depositDetectedTimestamp !== null) {
4278
+ return createLiquidiumStatus({
4279
+ operation: "deposit",
4280
+ state: "confirming"
4281
+ });
4282
+ }
4283
+ return createLiquidiumStatus({
4284
+ operation: "deposit",
4285
+ state: "action_required"
4286
+ });
4287
+ }
4288
+ function deriveActiveInstantLoanActivityStatus(input) {
4289
+ if (!input.started) {
4290
+ return findActiveActivityStatus(input.activeActivities, "deposit");
4291
+ }
4292
+ const borrowStatus = findActiveActivityStatus(
4293
+ input.activeActivities,
4294
+ "borrow"
4295
+ );
4296
+ if (borrowStatus) {
4297
+ return borrowStatus;
4298
+ }
4299
+ return findActiveActivityStatus(input.activeActivities, "repayment");
4300
+ }
4301
+ function findActiveActivityStatus(activities, operation) {
4302
+ const activity = activities.find(
4303
+ (candidate) => candidate.status.operation === operation
4304
+ );
4305
+ return activity?.status ?? null;
4306
+ }
4307
+ function isInstantLoanDepositExpired(input) {
4308
+ if (input.started || input.depositDetectedTimestamp === null) {
4309
+ return false;
4310
+ }
4311
+ if (input.expiryTimestamp === null) {
4312
+ return false;
4482
4313
  }
4483
- return InstantLoanStatus.awaitingDeposit;
4314
+ return input.expiryTimestamp <= getCurrentUnixTimestampSeconds();
4315
+ }
4316
+ function getCurrentUnixTimestampSeconds() {
4317
+ return BigInt(Math.floor(Date.now() / MILLISECONDS_PER_SECOND3));
4484
4318
  }
4485
4319
  function deriveDepositExpiryTimestamp(input) {
4486
4320
  if (input.depositDetectedTimestamp === null) {
@@ -4929,6 +4763,28 @@ function accountTypeToString(accountType) {
4929
4763
  }
4930
4764
  }
4931
4765
 
4766
+ // src/modules/lending/_internal/inflow-fee-rounding.ts
4767
+ var ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS = 100000n;
4768
+ var BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS = 500n;
4769
+ function roundInflowFeeEstimate(request, totalFee) {
4770
+ if (totalFee <= 0n) {
4771
+ return 0n;
4772
+ }
4773
+ if (isEthStablecoin(request.asset, request.chain)) {
4774
+ return roundUpToNearest(
4775
+ totalFee,
4776
+ ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS
4777
+ );
4778
+ }
4779
+ if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
4780
+ return roundUpToNearest(totalFee, BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS);
4781
+ }
4782
+ return totalFee;
4783
+ }
4784
+ function roundUpToNearest(amount, roundingUnit) {
4785
+ return ceilDivBigint(amount, roundingUnit) * roundingUnit;
4786
+ }
4787
+
4932
4788
  // src/core/utils/retry.ts
4933
4789
  var MIN_ATTEMPTS = 1;
4934
4790
  var MIN_INITIAL_RETRY_DELAY_MS = 0;
@@ -4980,835 +4836,966 @@ function assertValidRetryOptions(options) {
4980
4836
  }
4981
4837
  }
4982
4838
 
4983
- // src/modules/lending/_internal/inflow-fee-rounding.ts
4984
- var ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS = 100000n;
4985
- var BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS = 500n;
4986
- function roundInflowFeeEstimate(request, totalFee) {
4987
- if (totalFee <= 0n) {
4988
- return 0n;
4839
+ // src/modules/lending/_internal/supply-flow.ts
4840
+ var SUBMIT_INFLOW_MAX_ATTEMPTS = 4;
4841
+ var SUBMIT_INFLOW_INITIAL_RETRY_DELAY_MS = 1500;
4842
+ var SUBMIT_INFLOW_RETRY_BACKOFF_MULTIPLIER = 2;
4843
+ var ETH_APPROVAL_POLL_INTERVAL_MS = 2e3;
4844
+ var ETH_APPROVAL_MAX_POLLS = 30;
4845
+ var SupplyFlowExecutor = class {
4846
+ constructor(params) {
4847
+ this.params = params;
4989
4848
  }
4990
- if (isEthStablecoin(request.asset, request.chain)) {
4991
- return roundUpToNearest(
4992
- totalFee,
4993
- ETH_STABLECOIN_INFLOW_FEE_ROUND_UP_TO_NEAREST_10_CENTS
4849
+ params;
4850
+ async create(request) {
4851
+ const target = await resolveSupplyTarget(
4852
+ this.params.canisterContext,
4853
+ request
4994
4854
  );
4995
- }
4996
- if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
4997
- return roundUpToNearest(totalFee, BTC_INFLOW_FEE_ROUND_UP_TO_NEAREST_SATS);
4998
- }
4999
- return totalFee;
5000
- }
5001
- function roundUpToNearest(amount, roundingUnit) {
5002
- return ceilDivBigint(amount, roundingUnit) * roundingUnit;
5003
- }
5004
- function mapCanisterOutflowDetails(outflow) {
5005
- const rawOutflowType = getVariantKey(outflow.outflow_type);
5006
- return {
5007
- id: outflow.id,
5008
- outflowType: normalizeOutflowType(rawOutflowType),
5009
- outflowRef: outflow.outflow_ref[0],
5010
- txid: outflow.txid[0],
5011
- amount: outflow.amount,
5012
- receiver: mapCanisterAccountType(outflow.receiver)
5013
- };
5014
- }
5015
- function mapCanisterAccountType(receiver) {
5016
- if ("Native" in receiver) {
5017
- return {
5018
- type: "Native",
5019
- account: receiver.Native.toText()
4855
+ const instruction = {
4856
+ poolId: request.poolId,
4857
+ asset: target.asset,
4858
+ chain: target.chain,
4859
+ action: request.action,
4860
+ target
5020
4861
  };
5021
- }
5022
- if ("AccountIdentifier" in receiver) {
4862
+ const mechanism = resolveSupplyMechanism({
4863
+ asset: instruction.asset,
4864
+ chain: instruction.chain,
4865
+ requestedMechanism: request.mechanism
4866
+ });
4867
+ const defaultSubmitInflowRequest = getDefaultSubmitInflowRequest({
4868
+ action: request.action,
4869
+ chain: mechanism === SupplyPlanType.contractInteraction ? Chain.ETH : instruction.chain
4870
+ });
4871
+ let txid;
4872
+ switch (mechanism) {
4873
+ case SupplyPlanType.transfer:
4874
+ if (request.walletAdapter) {
4875
+ txid = await this.sendAndSubmitNativeSupplyInflow({
4876
+ request,
4877
+ instruction,
4878
+ defaultSubmitInflowRequest
4879
+ });
4880
+ }
4881
+ break;
4882
+ case SupplyPlanType.contractInteraction:
4883
+ txid = await this.executeContractSupply({
4884
+ request,
4885
+ instruction,
4886
+ defaultSubmitInflowRequest
4887
+ });
4888
+ break;
4889
+ }
5023
4890
  return {
5024
- type: "AccountIdentifier",
5025
- account: receiver.AccountIdentifier
4891
+ type: mechanism,
4892
+ target: instruction.target,
4893
+ txid,
4894
+ status: createLiquidiumStatus({
4895
+ operation: mapSupplyActionToStatusOperation(request.action),
4896
+ state: txid ? "confirming" : "action_required"
4897
+ }),
4898
+ submit: async (submitRequest) => {
4899
+ return await this.submitSupplyFlowInflow({
4900
+ instruction,
4901
+ mechanism,
4902
+ defaultSubmitInflowRequest,
4903
+ submitRequest
4904
+ });
4905
+ }
5026
4906
  };
5027
4907
  }
5028
- if ("Icrc" in receiver) {
5029
- const subaccount = normalizeOptionalSubaccount2(receiver.Icrc.subaccount[0]);
5030
- return {
5031
- type: "Icrc",
5032
- owner: receiver.Icrc.owner.toText(),
5033
- subaccount,
5034
- account: icrc.encodeIcrcAccount({
5035
- owner: receiver.Icrc.owner,
5036
- subaccount
5037
- })
5038
- };
5039
- }
5040
- return {
5041
- type: "External",
5042
- account: receiver.External
5043
- };
5044
- }
5045
- function normalizeOptionalSubaccount2(subaccount) {
5046
- if (!subaccount) {
5047
- return void 0;
5048
- }
5049
- return subaccount instanceof Uint8Array ? subaccount : Uint8Array.from(subaccount);
5050
- }
5051
- function normalizeOutflowType(rawOutflowType) {
5052
- switch (rawOutflowType) {
5053
- case "Withdraw":
5054
- return OutflowType.withdraw;
5055
- case "Borrow":
5056
- return OutflowType.borrow;
5057
- case "FeeClaim":
5058
- return OutflowType.feeClaim;
5059
- default:
5060
- throw new LiquidiumError(
5061
- LiquidiumErrorCode.INTERNAL,
5062
- `Unsupported outflow type: ${rawOutflowType}`
5063
- );
5064
- }
5065
- }
5066
- function mapWalletChainToLendingChain(chain) {
5067
- switch (chain) {
5068
- case Chain.BTC:
5069
- return { BTC: null };
5070
- case Chain.ETH:
5071
- return { ETH: null };
5072
- }
5073
- }
5074
-
5075
- // src/modules/lending/lending.ts
5076
- var SUBMIT_INFLOW_MAX_ATTEMPTS = 4;
5077
- var SUBMIT_INFLOW_INITIAL_RETRY_DELAY_MS = 1500;
5078
- var SUBMIT_INFLOW_RETRY_BACKOFF_MULTIPLIER = 2;
5079
- var ETH_APPROVAL_POLL_INTERVAL_MS = 2e3;
5080
- var ETH_APPROVAL_MAX_POLLS = 30;
5081
- var LendingModule = class {
5082
- constructor(canisterContext, apiClient, evmReadClient) {
5083
- this.canisterContext = canisterContext;
5084
- this.apiClient = apiClient;
5085
- this.evmReadClient = evmReadClient;
4908
+ async getEvmSupplyContext(request) {
4909
+ const selectedPool = await this.params.getPoolById(request.poolId);
4910
+ return await this.getEvmSupplyContextForPool({
4911
+ request,
4912
+ asset: selectedPool.asset,
4913
+ chain: selectedPool.chain
4914
+ });
5086
4915
  }
5087
- canisterContext;
5088
- apiClient;
5089
- evmReadClient;
5090
- /**
5091
- * Prepares a withdraw action that can be signed and submitted later.
5092
- *
5093
- * Use this when you need explicit control over signing and submission.
5094
- *
5095
- * @param request - Profile, pool, amount (pool asset base units), outflow address, and signer wallet.
5096
- * @returns A signable {@link WithdrawAction} with `submit` wired to the canister.
5097
- */
5098
- async prepareWithdraw(request) {
5099
- const destinationAccount = request.receiverAddress.trim();
5100
- const signerAccount = normalizeEvmAddress(
5101
- request.signerWalletAddress.trim()
4916
+ async getEvmSupplyContextForPool(params) {
4917
+ const { request, asset, chain } = params;
4918
+ const evmReadClient = this.requireEvmReadClient(
4919
+ "EVM supply context requires an EVM RPC URL or public client"
5102
4920
  );
5103
- if (!destinationAccount) {
4921
+ const walletAddress = normalizeAndValidateEvmAddress(
4922
+ request.walletAddress,
4923
+ "Invalid EVM wallet address"
4924
+ );
4925
+ if (request.amount <= 0n) {
5104
4926
  throw new LiquidiumError(
5105
4927
  LiquidiumErrorCode.VALIDATION_ERROR,
5106
- "Withdraw requires a custom outflow account"
4928
+ "EVM supply context requires a positive amount"
5107
4929
  );
5108
4930
  }
5109
- if (!signerAccount) {
4931
+ if (!isEthStablecoin(asset, chain)) {
5110
4932
  throw new LiquidiumError(
5111
4933
  LiquidiumErrorCode.VALIDATION_ERROR,
5112
- "Withdraw requires a signer account"
4934
+ `EVM supply context only supports ETH stablecoin pools, received ${asset} on ${chain}`
5113
4935
  );
5114
4936
  }
5115
- const receiverAddress = await this.normalizeOutflowReceiverAddress({
5116
- poolId: request.poolId,
5117
- receiverAddress: destinationAccount
4937
+ const tokenAddress = getEthStablecoinContractAddress(asset);
4938
+ const spenderAddress = CK_ETH_DEPOSIT_CONTRACT_ADDRESS;
4939
+ const [allowance, balance] = await Promise.all([
4940
+ readErc20Allowance({
4941
+ evmReadClient,
4942
+ tokenAddress,
4943
+ ownerAddress: walletAddress,
4944
+ spenderAddress
4945
+ }),
4946
+ readErc20Balance({
4947
+ evmReadClient,
4948
+ tokenAddress,
4949
+ ownerAddress: walletAddress
4950
+ })
4951
+ ]);
4952
+ const approvalStrategy = getApprovalStrategy({
4953
+ allowance,
4954
+ amount: request.amount
5118
4955
  });
5119
- const lendingActor = createLendingActor(this.canisterContext);
5120
- try {
5121
- const expiryTimestamp = computeExpiryTimestampFromNow();
5122
- const nonce = await lendingActor.get_nonce(signerAccount);
5123
- const withdrawRequestData = {
5124
- profileId: request.profileId,
5125
- poolId: request.poolId,
5126
- amount: request.amount,
5127
- receiverAddress,
5128
- signerWalletAddress: signerAccount,
5129
- expiryTimestamp
5130
- };
5131
- return {
5132
- kind: WalletActionKind.createWithdraw,
5133
- executionKind: WalletExecutionKind.signMessage,
5134
- actionType: WalletActionKind.createWithdraw,
5135
- transferMode: TransferMode.native,
5136
- account: signerAccount,
5137
- message: createWithdrawAssetMessage(
5138
- {
5139
- pool_id: request.poolId,
5140
- amount: request.amount.toString(),
5141
- account: { type: "External", data: receiverAddress },
5142
- expiry_timestamp: expiryTimestamp
5143
- },
5144
- nonce
5145
- ),
5146
- data: withdrawRequestData,
5147
- submit: async (signatureInfo) => {
5148
- return await this.submitWithdraw(withdrawRequestData, signatureInfo);
5149
- }
5150
- };
5151
- } catch (error) {
5152
- if (error instanceof LiquidiumError) {
5153
- throw error;
5154
- }
5155
- throw mapCanisterCallErrorToLiquidiumError("get_nonce", error);
5156
- }
5157
- }
5158
- async submitWithdraw(request, signatureInfo) {
5159
- try {
5160
- const result = await createLendingActor(this.canisterContext).withdraw(
5161
- principal.Principal.fromText(request.profileId),
5162
- {
5163
- data: {
5164
- expiry_timestamp: request.expiryTimestamp,
5165
- account: { External: request.receiverAddress },
5166
- pool_id: principal.Principal.fromText(request.poolId),
5167
- amount: request.amount
5168
- },
5169
- signature_info: {
5170
- Wallet: {
5171
- signature: normalizeWalletSignature(
5172
- signatureInfo.signature,
5173
- signatureInfo.chain
5174
- ),
5175
- chain: mapWalletChainToLendingChain(signatureInfo.chain),
5176
- account: request.signerWalletAddress
5177
- }
5178
- }
5179
- }
5180
- );
5181
- if ("Err" in result) {
5182
- throw mapLendingProtocolErrorToLiquidiumError(result.Err);
5183
- }
5184
- return mapExpectedOutflowDetails(
5185
- mapCanisterOutflowDetails(result.Ok),
5186
- OutflowType.withdraw,
5187
- "withdraw"
5188
- );
5189
- } catch (error) {
5190
- if (error instanceof LiquidiumError) {
5191
- throw error;
5192
- }
5193
- throw mapCanisterCallErrorToLiquidiumError("withdraw", error);
5194
- }
5195
- }
5196
- /**
5197
- * Creates a withdraw outflow using the provided wallet adapter.
5198
- *
5199
- * This is the convenience form of `prepareWithdraw(...)` plus execution.
5200
- *
5201
- * @param params - Withdraw request fields plus `signerChain` and `signerWalletAdapter`.
5202
- * @returns The canister {@link OutflowDetails} for the completed withdraw.
5203
- */
5204
- async withdraw(params) {
5205
- const action = await this.prepareWithdraw(params);
5206
- return await executeWith({
5207
- walletAdapter: params.signerWalletAdapter,
5208
- chain: params.signerChain,
5209
- account: action.account
5210
- })(action);
4956
+ return {
4957
+ profileId: request.profileId,
4958
+ poolId: request.poolId,
4959
+ walletAddress,
4960
+ action: request.action,
4961
+ asset,
4962
+ chain: Chain.ETH,
4963
+ amount: request.amount.toString(),
4964
+ tokenAddress,
4965
+ spenderAddress,
4966
+ depositContractAddress: CK_ETH_DEPOSIT_CONTRACT_ADDRESS,
4967
+ balance: balance.toString(),
4968
+ allowance: allowance.toString(),
4969
+ requiresApproval: approvalStrategy !== EvmSupplyApprovalStrategy.none,
4970
+ approvalStrategy
4971
+ };
5211
4972
  }
5212
- /**
5213
- * Prepares a borrow action that can be signed and submitted later.
5214
- *
5215
- * Use this when you need explicit control over signing and submission.
5216
- *
5217
- * @param request - Profile, pool, amount (borrow asset base units), outflow address, and signer wallet.
5218
- * @returns A signable {@link BorrowAction} with `submit` wired to the canister.
5219
- */
5220
- async prepareBorrow(request) {
5221
- const destinationAccount = request.receiverAddress.trim();
5222
- const signerAccount = normalizeEvmAddress(
5223
- request.signerWalletAddress.trim()
5224
- );
5225
- if (!destinationAccount) {
4973
+ async sendAndSubmitNativeSupplyInflow(params) {
4974
+ const { request, instruction, defaultSubmitInflowRequest } = params;
4975
+ if (instruction.target.type !== "nativeAddress") {
5226
4976
  throw new LiquidiumError(
5227
4977
  LiquidiumErrorCode.VALIDATION_ERROR,
5228
- "Borrow requires a custom outflow account"
4978
+ "Wallet-executed supply requires a native-address target"
5229
4979
  );
5230
4980
  }
5231
- if (!signerAccount) {
4981
+ if (!request.walletAdapter) {
5232
4982
  throw new LiquidiumError(
5233
4983
  LiquidiumErrorCode.VALIDATION_ERROR,
5234
- "Borrow requires a signer account"
4984
+ "Wallet-executed supply requires a wallet adapter"
5235
4985
  );
5236
4986
  }
5237
- if (request.amount <= 0n) {
4987
+ const account = request.account?.trim();
4988
+ if (!account) {
5238
4989
  throw new LiquidiumError(
5239
4990
  LiquidiumErrorCode.VALIDATION_ERROR,
5240
- "Borrow amount must be greater than 0"
4991
+ "Wallet-executed transfer supply requires an account"
5241
4992
  );
5242
4993
  }
5243
- const selectedPool = await this.getPoolById(request.poolId);
5244
- const selectedAsset = selectedPool.asset;
5245
- const minimumBorrowAmountError = getBorrowAmountMinimumValidationError({
5246
- amount: request.amount,
5247
- asset: selectedAsset
5248
- });
5249
- if (minimumBorrowAmountError) {
4994
+ if (!request.amount || request.amount <= 0n) {
5250
4995
  throw new LiquidiumError(
5251
4996
  LiquidiumErrorCode.VALIDATION_ERROR,
5252
- minimumBorrowAmountError.message
4997
+ "Wallet-executed supply requires a positive amount"
5253
4998
  );
5254
4999
  }
5255
- const receiverAddress = normalizeExternalAddress({
5256
- address: destinationAccount,
5257
- asset: selectedAsset,
5258
- chain: selectedPool.chain
5000
+ const txid = await this.sendNativeSupplyTransaction({
5001
+ walletAdapter: request.walletAdapter,
5002
+ chain: instruction.chain,
5003
+ toAddress: instruction.target.address,
5004
+ amount: request.amount,
5005
+ senderAccount: account,
5006
+ asset: instruction.asset,
5007
+ action: request.action
5259
5008
  });
5260
- const lendingActor = createLendingActor(this.canisterContext);
5261
- try {
5262
- const expiryTimestamp = computeExpiryTimestampFromNow();
5263
- const nonce = await lendingActor.get_nonce(signerAccount);
5264
- const borrowRequestData = {
5265
- profileId: request.profileId,
5266
- poolId: request.poolId,
5267
- amount: request.amount,
5268
- receiverAddress,
5269
- signerWalletAddress: signerAccount,
5270
- expiryTimestamp
5271
- };
5009
+ if (shouldSubmitInflow({ instruction, mechanism: SupplyPlanType.transfer })) {
5010
+ try {
5011
+ await this.submitInflowWithRetry(txid, defaultSubmitInflowRequest);
5012
+ } catch {
5013
+ }
5014
+ }
5015
+ return txid;
5016
+ }
5017
+ async submitSupplyFlowInflow(params) {
5018
+ if (!shouldSubmitInflow(params)) {
5272
5019
  return {
5273
- kind: WalletActionKind.createBorrow,
5274
- executionKind: WalletExecutionKind.signMessage,
5275
- actionType: WalletActionKind.createBorrow,
5276
- transferMode: TransferMode.native,
5277
- account: signerAccount,
5278
- message: createBorrowAssetMessage(
5279
- {
5280
- pool_id: request.poolId,
5281
- amount: request.amount.toString(),
5282
- account: { type: "External", data: receiverAddress },
5283
- expiry_timestamp: expiryTimestamp
5284
- },
5285
- nonce
5286
- ),
5287
- data: borrowRequestData,
5288
- submit: async (signatureInfo) => {
5289
- return await this.submitBorrow(borrowRequestData, signatureInfo);
5290
- }
5020
+ txid: params.submitRequest.txid
5291
5021
  };
5292
- } catch (error) {
5293
- if (error instanceof LiquidiumError) {
5294
- throw error;
5295
- }
5296
- throw mapCanisterCallErrorToLiquidiumError("get_nonce", error);
5297
5022
  }
5023
+ const defaultSubmitInflowRequest = params.defaultSubmitInflowRequest;
5024
+ if (!defaultSubmitInflowRequest) {
5025
+ throw new LiquidiumError(
5026
+ LiquidiumErrorCode.VALIDATION_ERROR,
5027
+ "Supply flow submit requires an inflow operation"
5028
+ );
5029
+ }
5030
+ return await this.params.submitInflow({
5031
+ ...defaultSubmitInflowRequest,
5032
+ ...params.submitRequest
5033
+ });
5298
5034
  }
5299
- async submitBorrow(request, signatureInfo) {
5035
+ async executeContractSupply(params) {
5036
+ const { request, instruction, defaultSubmitInflowRequest } = params;
5037
+ if (instruction.target.type !== "icrcAccount" || instruction.chain !== Chain.ETH) {
5038
+ throw new LiquidiumError(
5039
+ LiquidiumErrorCode.VALIDATION_ERROR,
5040
+ "Contract-interaction supply is only supported for ETH ICRC-account targets"
5041
+ );
5042
+ }
5043
+ const walletAddressInput = request.account?.trim();
5044
+ if (!walletAddressInput) {
5045
+ throw new LiquidiumError(
5046
+ LiquidiumErrorCode.VALIDATION_ERROR,
5047
+ "Contract-interaction supply requires an account"
5048
+ );
5049
+ }
5050
+ const walletAddress = normalizeAndValidateEvmAddress(
5051
+ walletAddressInput,
5052
+ "Invalid EVM wallet address"
5053
+ );
5054
+ if (!request.amount || request.amount <= 0n) {
5055
+ throw new LiquidiumError(
5056
+ LiquidiumErrorCode.VALIDATION_ERROR,
5057
+ "Contract-interaction supply requires a positive amount"
5058
+ );
5059
+ }
5060
+ const walletAdapter = request.walletAdapter;
5061
+ if (!walletAdapter?.sendEthTransaction) {
5062
+ throw new LiquidiumError(
5063
+ LiquidiumErrorCode.VALIDATION_ERROR,
5064
+ "Contract-interaction supply requires an ETH wallet adapter"
5065
+ );
5066
+ }
5067
+ this.params.requireApi();
5068
+ this.requireEvmReadClient(
5069
+ "Contract-interaction supply requires an EVM RPC URL or public client"
5070
+ );
5071
+ const evmSupplyContext = await this.getEvmSupplyContextForPool({
5072
+ request: {
5073
+ profileId: request.profileId,
5074
+ poolId: request.poolId,
5075
+ walletAddress,
5076
+ amount: request.amount,
5077
+ action: request.action
5078
+ },
5079
+ asset: instruction.asset,
5080
+ chain: instruction.chain
5081
+ });
5082
+ const supplyAmount = BigInt(evmSupplyContext.amount);
5083
+ if (BigInt(evmSupplyContext.balance) < supplyAmount) {
5084
+ throw new LiquidiumError(
5085
+ LiquidiumErrorCode.INSUFFICIENT_FUNDS,
5086
+ `Insufficient ${evmSupplyContext.asset} balance for ${request.action}`
5087
+ );
5088
+ }
5089
+ if (evmSupplyContext.approvalStrategy === EvmSupplyApprovalStrategy.resetThenApproveMax) {
5090
+ await this.sendEthContractTransaction(
5091
+ walletAdapter,
5092
+ walletAddress,
5093
+ createApproveTransaction({
5094
+ tokenAddress: evmSupplyContext.tokenAddress,
5095
+ spenderAddress: evmSupplyContext.spenderAddress,
5096
+ amount: 0n
5097
+ }),
5098
+ `supply-${request.action}-approve-reset`
5099
+ );
5100
+ await this.waitForExpectedAllowance({
5101
+ walletAddress,
5102
+ tokenAddress: evmSupplyContext.tokenAddress,
5103
+ spenderAddress: evmSupplyContext.spenderAddress,
5104
+ amount: supplyAmount,
5105
+ expectation: "zero"
5106
+ });
5107
+ }
5108
+ if (evmSupplyContext.approvalStrategy !== EvmSupplyApprovalStrategy.none) {
5109
+ await this.sendEthContractTransaction(
5110
+ walletAdapter,
5111
+ walletAddress,
5112
+ createApproveTransaction({
5113
+ tokenAddress: evmSupplyContext.tokenAddress,
5114
+ spenderAddress: evmSupplyContext.spenderAddress,
5115
+ amount: MAX_UINT256
5116
+ }),
5117
+ `supply-${request.action}-approve-max`
5118
+ );
5119
+ await this.waitForExpectedAllowance({
5120
+ walletAddress,
5121
+ tokenAddress: evmSupplyContext.tokenAddress,
5122
+ spenderAddress: evmSupplyContext.spenderAddress,
5123
+ amount: supplyAmount,
5124
+ expectation: "sufficient"
5125
+ });
5126
+ }
5127
+ const depositTxid = await this.sendEthContractTransaction(
5128
+ walletAdapter,
5129
+ walletAddress,
5130
+ createDepositErc20Transaction({
5131
+ depositContractAddress: evmSupplyContext.depositContractAddress,
5132
+ tokenAddress: evmSupplyContext.tokenAddress,
5133
+ amount: supplyAmount,
5134
+ poolId: request.poolId,
5135
+ profileId: request.profileId,
5136
+ destinationAccount: instruction.target.account,
5137
+ action: request.action
5138
+ }),
5139
+ `supply-${request.action}-deposit-erc20`
5140
+ );
5300
5141
  try {
5301
- const result = await createLendingActor(
5302
- this.canisterContext
5303
- ).borrow_assets(principal.Principal.fromText(request.profileId), {
5304
- data: {
5305
- expiry_timestamp: request.expiryTimestamp,
5306
- account: { External: request.receiverAddress },
5307
- pool_id: principal.Principal.fromText(request.poolId),
5308
- amount: request.amount
5309
- },
5310
- signature_info: {
5311
- Wallet: {
5312
- signature: normalizeWalletSignature(
5313
- signatureInfo.signature,
5314
- signatureInfo.chain
5315
- ),
5316
- chain: mapWalletChainToLendingChain(signatureInfo.chain),
5317
- account: request.signerWalletAddress
5318
- }
5142
+ await this.submitInflowWithRetry(depositTxid, defaultSubmitInflowRequest);
5143
+ } catch {
5144
+ }
5145
+ return depositTxid;
5146
+ }
5147
+ async sendNativeSupplyTransaction(params) {
5148
+ switch (params.chain) {
5149
+ case Chain.BTC: {
5150
+ if (!params.walletAdapter.sendBtcTransaction) {
5151
+ throw new LiquidiumError(
5152
+ LiquidiumErrorCode.VALIDATION_ERROR,
5153
+ "BTC wallet adapter does not support transaction sending"
5154
+ );
5319
5155
  }
5320
- });
5321
- if ("Err" in result) {
5322
- throw mapLendingProtocolErrorToLiquidiumError(result.Err);
5156
+ return await params.walletAdapter.sendBtcTransaction({
5157
+ chain: Chain.BTC,
5158
+ toAddress: params.toAddress,
5159
+ amountSats: params.amount,
5160
+ account: params.senderAccount,
5161
+ actionType: `supply-${params.action}`,
5162
+ transferMode: TransferMode.native
5163
+ });
5323
5164
  }
5324
- return mapExpectedOutflowDetails(
5325
- mapCanisterOutflowDetails(result.Ok),
5326
- OutflowType.borrow,
5327
- "borrow_assets"
5328
- );
5329
- } catch (error) {
5330
- if (error instanceof LiquidiumError) {
5331
- throw error;
5165
+ case Chain.ETH: {
5166
+ if (!params.walletAdapter.sendEthTransaction) {
5167
+ throw new LiquidiumError(
5168
+ LiquidiumErrorCode.VALIDATION_ERROR,
5169
+ "ETH wallet adapter does not support transaction sending"
5170
+ );
5171
+ }
5172
+ if (isEthStablecoin(params.asset, params.chain)) {
5173
+ return await params.walletAdapter.sendEthTransaction({
5174
+ chain: Chain.ETH,
5175
+ account: params.senderAccount,
5176
+ actionType: `supply-${params.action}`,
5177
+ transferMode: TransferMode.native,
5178
+ transaction: createTransferErc20Transaction({
5179
+ tokenAddress: getEthStablecoinContractAddress(params.asset),
5180
+ recipientAddress: params.toAddress,
5181
+ amount: params.amount
5182
+ })
5183
+ });
5184
+ }
5185
+ return await params.walletAdapter.sendEthTransaction({
5186
+ chain: Chain.ETH,
5187
+ account: params.senderAccount,
5188
+ actionType: `supply-${params.action}`,
5189
+ transferMode: TransferMode.native,
5190
+ transaction: {
5191
+ to: params.toAddress,
5192
+ value: params.amount.toString()
5193
+ }
5194
+ });
5332
5195
  }
5333
- throw mapCanisterCallErrorToLiquidiumError("borrow_assets", error);
5196
+ default:
5197
+ throw new LiquidiumError(
5198
+ LiquidiumErrorCode.VALIDATION_ERROR,
5199
+ `Native-address wallet execution is not supported for ${params.chain}`
5200
+ );
5334
5201
  }
5335
5202
  }
5336
- /**
5337
- * Creates a borrow outflow using the provided wallet adapter.
5338
- *
5339
- * This is the convenience form of `prepareBorrow(...)` plus execution.
5340
- *
5341
- * @param params - Borrow request fields plus `signerChain` and `signerWalletAdapter`.
5342
- * @returns The lending canister receipt as {@link OutflowDetails}.
5343
- *
5344
- * @remarks
5345
- * `id` is always present. `txid` may be missing on the first response; the SDK does not
5346
- * poll for it. Use history or app-level polling if you need the chain transaction id.
5347
- */
5348
- async borrow(params) {
5349
- const action = await this.prepareBorrow(params);
5350
- return await executeWith({
5351
- walletAdapter: params.signerWalletAdapter,
5352
- chain: params.signerChain,
5353
- account: action.account
5354
- })(action);
5355
- }
5356
- /**
5357
- * Resolves a supply target for a deposit or repayment and optionally broadcasts it.
5358
- *
5359
- * Transfer mode can return manual broadcast instructions when wallet fields are
5360
- * omitted. Contract-interaction mode always requires `walletAdapter`, `account`,
5361
- * and `amount` because it must prepare and submit approval/deposit calls.
5362
- *
5363
- * The SDK does not poll for inflow status. When a `txid` is returned, it is the
5364
- * caller's responsibility to track confirmation state using their own polling.
5365
- *
5366
- * @returns A {@link SupplyFlow} receipt with `type`, `target`, `submit`, and
5367
- * an optional `txid` present when the SDK broadcast for you.
5368
- */
5369
- async supply(request) {
5370
- const target = await resolveSupplyTarget(this.canisterContext, request);
5371
- const instruction = {
5372
- poolId: request.poolId,
5373
- asset: target.asset,
5374
- chain: target.chain,
5375
- action: request.action,
5376
- target
5377
- };
5378
- const mechanism = resolveSupplyMechanism({
5379
- asset: instruction.asset,
5380
- chain: instruction.chain,
5381
- requestedMechanism: request.mechanism
5203
+ async submitInflowWithRetry(txid, extraRequest) {
5204
+ if (!extraRequest) {
5205
+ throw new LiquidiumError(
5206
+ LiquidiumErrorCode.VALIDATION_ERROR,
5207
+ "Inflow submission requires an operation"
5208
+ );
5209
+ }
5210
+ await retryWithBackoff({
5211
+ execute: async () => {
5212
+ await this.params.submitInflow({ txid, ...extraRequest });
5213
+ },
5214
+ maxAttempts: SUBMIT_INFLOW_MAX_ATTEMPTS,
5215
+ initialRetryDelayMs: SUBMIT_INFLOW_INITIAL_RETRY_DELAY_MS,
5216
+ backoffMultiplier: SUBMIT_INFLOW_RETRY_BACKOFF_MULTIPLIER,
5217
+ shouldRetryError: isRetriableInflowSubmitError
5382
5218
  });
5383
- const defaultSubmitInflowRequest = getDefaultSubmitInflowRequest({
5384
- action: request.action,
5385
- chain: mechanism === SupplyPlanType.contractInteraction ? Chain.ETH : instruction.chain
5219
+ }
5220
+ async sendEthContractTransaction(walletAdapter, walletAddress, request, actionType) {
5221
+ if (!walletAdapter.sendEthTransaction) {
5222
+ throw new LiquidiumError(
5223
+ LiquidiumErrorCode.VALIDATION_ERROR,
5224
+ "ETH wallet adapter does not support transaction sending"
5225
+ );
5226
+ }
5227
+ return await walletAdapter.sendEthTransaction({
5228
+ chain: Chain.ETH,
5229
+ account: walletAddress,
5230
+ actionType,
5231
+ transferMode: TransferMode.native,
5232
+ transaction: request
5386
5233
  });
5387
- let txid;
5388
- switch (mechanism) {
5389
- case SupplyPlanType.transfer:
5390
- if (request.walletAdapter) {
5391
- txid = await this.sendAndSubmitNativeSupplyInflow({
5392
- request,
5393
- instruction,
5394
- defaultSubmitInflowRequest
5395
- });
5396
- }
5397
- break;
5398
- case SupplyPlanType.contractInteraction:
5399
- txid = await this.executeContractSupply({
5400
- request,
5401
- instruction,
5402
- defaultSubmitInflowRequest
5234
+ }
5235
+ async waitForExpectedAllowance(params) {
5236
+ const evmReadClient = this.requireEvmReadClient(
5237
+ "Contract-interaction supply requires an EVM RPC URL or public client"
5238
+ );
5239
+ let lastPollingError;
5240
+ for (let pollIndex = 0; pollIndex < ETH_APPROVAL_MAX_POLLS; pollIndex += 1) {
5241
+ try {
5242
+ const allowance = await readErc20Allowance({
5243
+ evmReadClient,
5244
+ tokenAddress: params.tokenAddress,
5245
+ ownerAddress: params.walletAddress,
5246
+ spenderAddress: params.spenderAddress
5403
5247
  });
5404
- break;
5248
+ const matchesExpectation = params.expectation === "zero" ? allowance === 0n : allowance >= params.amount;
5249
+ if (matchesExpectation) {
5250
+ return;
5251
+ }
5252
+ } catch (error) {
5253
+ lastPollingError = error;
5254
+ }
5255
+ await delay(ETH_APPROVAL_POLL_INTERVAL_MS);
5405
5256
  }
5257
+ const lastPollingErrorMessage = getUnknownErrorMessage(lastPollingError);
5258
+ throw new LiquidiumError(
5259
+ LiquidiumErrorCode.SERVICE_UNAVAILABLE,
5260
+ lastPollingErrorMessage ? `Timed out waiting for ${params.expectation} ETH allowance update. Last polling error: ${lastPollingErrorMessage}` : `Timed out waiting for ${params.expectation} ETH allowance update`
5261
+ );
5262
+ }
5263
+ requireEvmReadClient(message) {
5264
+ if (!this.params.evmReadClient) {
5265
+ throw new LiquidiumError(LiquidiumErrorCode.VALIDATION_ERROR, message);
5266
+ }
5267
+ return this.params.evmReadClient;
5268
+ }
5269
+ };
5270
+ async function readErc20Allowance(params) {
5271
+ const allowance = await params.evmReadClient.readContract({
5272
+ address: params.tokenAddress,
5273
+ abi: ERC20_ABI,
5274
+ functionName: "allowance",
5275
+ args: [
5276
+ params.ownerAddress,
5277
+ params.spenderAddress
5278
+ ]
5279
+ });
5280
+ return BigInt(allowance);
5281
+ }
5282
+ async function readErc20Balance(params) {
5283
+ const balance = await params.evmReadClient.readContract({
5284
+ address: params.tokenAddress,
5285
+ abi: ERC20_ABI,
5286
+ functionName: "balanceOf",
5287
+ args: [params.ownerAddress]
5288
+ });
5289
+ return BigInt(balance);
5290
+ }
5291
+ function getApprovalStrategy(params) {
5292
+ if (params.allowance >= params.amount) {
5293
+ return EvmSupplyApprovalStrategy.none;
5294
+ }
5295
+ if (params.allowance === 0n) {
5296
+ return EvmSupplyApprovalStrategy.approveMax;
5297
+ }
5298
+ return EvmSupplyApprovalStrategy.resetThenApproveMax;
5299
+ }
5300
+ async function delay(timeoutMs) {
5301
+ await new Promise((resolve) => setTimeout(resolve, timeoutMs));
5302
+ }
5303
+ function isRetriableInflowSubmitError(error) {
5304
+ if (!(error instanceof LiquidiumError)) {
5305
+ return false;
5306
+ }
5307
+ return error.code === LiquidiumErrorCode.SERVICE_UNAVAILABLE;
5308
+ }
5309
+ function getUnknownErrorMessage(error) {
5310
+ if (error instanceof Error) {
5311
+ return error.message;
5312
+ }
5313
+ if (typeof error === "string") {
5314
+ return error;
5315
+ }
5316
+ return null;
5317
+ }
5318
+ function getDefaultSubmitInflowRequest(params) {
5319
+ if (params.chain !== Chain.BTC && params.chain !== Chain.ETH) {
5320
+ return void 0;
5321
+ }
5322
+ return {
5323
+ chain: params.chain,
5324
+ operation: mapSupplyActionToStatusOperation(params.action)
5325
+ };
5326
+ }
5327
+ function mapSupplyActionToStatusOperation(action) {
5328
+ return action === SupplyAction.repayment ? "repayment" : "deposit";
5329
+ }
5330
+ function shouldSubmitInflow(params) {
5331
+ if (params.mechanism !== SupplyPlanType.transfer) {
5332
+ return true;
5333
+ }
5334
+ return !isEthStablecoin(params.instruction.asset, params.instruction.chain);
5335
+ }
5336
+ function mapCanisterOutflowDetails(outflow) {
5337
+ const rawOutflowType = getVariantKey(outflow.outflow_type);
5338
+ return {
5339
+ id: outflow.id,
5340
+ outflowType: normalizeOutflowType(rawOutflowType),
5341
+ outflowRef: outflow.outflow_ref[0],
5342
+ txid: outflow.txid[0],
5343
+ amount: outflow.amount,
5344
+ receiver: mapCanisterAccountType(outflow.receiver)
5345
+ };
5346
+ }
5347
+ function mapCanisterAccountType(receiver) {
5348
+ if ("Native" in receiver) {
5406
5349
  return {
5407
- type: mechanism,
5408
- target: instruction.target,
5409
- txid,
5410
- submit: async (submitRequest) => {
5411
- return await this.submitSupplyFlowInflow({
5412
- instruction,
5413
- mechanism,
5414
- defaultSubmitInflowRequest,
5415
- submitRequest
5416
- });
5417
- }
5350
+ type: "Native",
5351
+ account: receiver.Native.toText()
5352
+ };
5353
+ }
5354
+ if ("AccountIdentifier" in receiver) {
5355
+ return {
5356
+ type: "AccountIdentifier",
5357
+ account: receiver.AccountIdentifier
5418
5358
  };
5419
5359
  }
5360
+ if ("Icrc" in receiver) {
5361
+ const subaccount = normalizeOptionalSubaccount2(receiver.Icrc.subaccount[0]);
5362
+ return {
5363
+ type: "Icrc",
5364
+ owner: receiver.Icrc.owner.toText(),
5365
+ subaccount,
5366
+ account: icrc.encodeIcrcAccount({
5367
+ owner: receiver.Icrc.owner,
5368
+ subaccount
5369
+ })
5370
+ };
5371
+ }
5372
+ return {
5373
+ type: "External",
5374
+ account: receiver.External
5375
+ };
5376
+ }
5377
+ function normalizeOptionalSubaccount2(subaccount) {
5378
+ if (!subaccount) {
5379
+ return void 0;
5380
+ }
5381
+ return subaccount instanceof Uint8Array ? subaccount : Uint8Array.from(subaccount);
5382
+ }
5383
+ function normalizeOutflowType(rawOutflowType) {
5384
+ switch (rawOutflowType) {
5385
+ case "Withdraw":
5386
+ return OutflowType.withdrawal;
5387
+ case "Borrow":
5388
+ return OutflowType.borrow;
5389
+ case "FeeClaim":
5390
+ return OutflowType.feeClaim;
5391
+ default:
5392
+ throw new LiquidiumError(
5393
+ LiquidiumErrorCode.INTERNAL,
5394
+ `Unsupported outflow type: ${rawOutflowType}`
5395
+ );
5396
+ }
5397
+ }
5398
+ function mapWalletChainToLendingChain(chain) {
5399
+ switch (chain) {
5400
+ case Chain.BTC:
5401
+ return { BTC: null };
5402
+ case Chain.ETH:
5403
+ return { ETH: null };
5404
+ }
5405
+ }
5406
+
5407
+ // src/modules/lending/lending.ts
5408
+ var LendingModule = class {
5409
+ constructor(canisterContext, apiClient, evmReadClient) {
5410
+ this.canisterContext = canisterContext;
5411
+ this.apiClient = apiClient;
5412
+ this.evmReadClient = evmReadClient;
5413
+ }
5414
+ canisterContext;
5415
+ apiClient;
5416
+ evmReadClient;
5420
5417
  /**
5421
- * Fetches ERC-20 supply planning data with the configured EVM read client.
5418
+ * Prepares a withdraw action that can be signed and submitted later.
5422
5419
  *
5423
- * Requires `evmRpcUrl` or `evmPublicClient` on the client. Used internally by
5424
- * contract-interaction `supply`.
5420
+ * Use this when you need explicit control over signing and submission.
5425
5421
  *
5426
- * @param request - Profile, pool, wallet, amount (token base units), and action.
5427
- * @returns Locally computed {@link EvmSupplyContext} for approvals and deposit.
5422
+ * @param request - Profile, pool, amount (pool asset base units), outflow address, and signer wallet.
5423
+ * @returns A signable {@link WithdrawAction} with `submit` wired to the canister.
5428
5424
  */
5429
- async getEvmSupplyContext(request) {
5430
- const selectedPool = await this.getPoolById(request.poolId);
5431
- return await this.getEvmSupplyContextForPool({
5432
- request,
5433
- asset: selectedPool.asset,
5434
- chain: selectedPool.chain
5435
- });
5436
- }
5437
- async getEvmSupplyContextForPool(params) {
5438
- const { request, asset, chain } = params;
5439
- const evmReadClient = this.requireEvmReadClient(
5440
- "EVM supply context requires an EVM RPC URL or public client"
5441
- );
5442
- const walletAddress = normalizeAndValidateEvmAddress(
5443
- request.walletAddress,
5444
- "Invalid EVM wallet address"
5425
+ async prepareWithdraw(request) {
5426
+ const destinationAccount = request.receiverAddress.trim();
5427
+ const signerAccount = normalizeEvmAddress(
5428
+ request.signerWalletAddress.trim()
5445
5429
  );
5446
- if (request.amount <= 0n) {
5430
+ if (!destinationAccount) {
5447
5431
  throw new LiquidiumError(
5448
5432
  LiquidiumErrorCode.VALIDATION_ERROR,
5449
- "EVM supply context requires a positive amount"
5433
+ "Withdraw requires a custom outflow account"
5450
5434
  );
5451
5435
  }
5452
- if (!isEthStablecoin(asset, chain)) {
5436
+ if (!signerAccount) {
5453
5437
  throw new LiquidiumError(
5454
5438
  LiquidiumErrorCode.VALIDATION_ERROR,
5455
- `EVM supply context only supports ETH stablecoin pools, received ${asset} on ${chain}`
5439
+ "Withdraw requires a signer account"
5456
5440
  );
5457
5441
  }
5458
- const tokenAddress = getEthStablecoinContractAddress(asset);
5459
- const spenderAddress = CK_ETH_DEPOSIT_CONTRACT_ADDRESS;
5460
- const [allowance, balance] = await Promise.all([
5461
- readErc20Allowance({
5462
- evmReadClient,
5463
- tokenAddress,
5464
- ownerAddress: walletAddress,
5465
- spenderAddress
5466
- }),
5467
- readErc20Balance({
5468
- evmReadClient,
5469
- tokenAddress,
5470
- ownerAddress: walletAddress
5471
- })
5472
- ]);
5473
- const approvalStrategy = getApprovalStrategy({
5474
- allowance,
5475
- amount: request.amount
5476
- });
5477
- return {
5478
- success: true,
5479
- profileId: request.profileId,
5442
+ const receiverAddress = await this.normalizeOutflowReceiverAddress({
5480
5443
  poolId: request.poolId,
5481
- walletAddress,
5482
- action: request.action,
5483
- asset,
5484
- chain: Chain.ETH,
5485
- amount: request.amount.toString(),
5486
- tokenAddress,
5487
- spenderAddress,
5488
- depositContractAddress: CK_ETH_DEPOSIT_CONTRACT_ADDRESS,
5489
- balance: balance.toString(),
5490
- allowance: allowance.toString(),
5491
- requiresApproval: approvalStrategy !== EvmSupplyApprovalStrategy.none,
5492
- approvalStrategy
5493
- };
5494
- }
5495
- /**
5496
- * Returns the read-only deposit address for an ETH stablecoin inflow target.
5497
- *
5498
- * This is a query call that does not create or mutate state. Use it when you
5499
- * need the deposit address without hitting the authorization-gated update path.
5500
- *
5501
- * @param request - Profile, pool, asset, and supply action.
5502
- * @returns The EVM deposit address for the derived account.
5503
- */
5504
- async getDepositAddress(request) {
5505
- if (!isEthStablecoin(request.asset, Chain.ETH)) {
5506
- throw new LiquidiumError(
5507
- LiquidiumErrorCode.VALIDATION_ERROR,
5508
- "getDepositAddress is only supported for ETH stablecoins"
5509
- );
5510
- }
5511
- const tokenAddress = getEthStablecoinContractAddress(request.asset);
5512
- const subaccount = encodeInflowSubaccount({
5513
- action: request.action,
5514
- principal: principal.Principal.fromText(request.profileId)
5444
+ receiverAddress: destinationAccount
5515
5445
  });
5516
- const result = await createDepositAccountsActor(
5517
- this.canisterContext
5518
- ).get_deposit_address(
5519
- {
5520
- owner: principal.Principal.fromText(request.poolId),
5521
- subaccount: [subaccount]
5522
- },
5523
- [tokenAddress]
5524
- );
5525
- if ("Err" in result) {
5526
- throw mapDepositAccountErrorToLiquidiumError(result.Err);
5527
- }
5528
- return result.Ok;
5529
- }
5530
- /**
5531
- * Estimates the network/deposit fee for an inflow target.
5532
- *
5533
- * ETH stablecoin deposit-address estimates are served by the deposit-address
5534
- * canister. BTC estimates include the ckBTC minter deposit fee and ledger fee.
5535
- *
5536
- * @param request - Asset and chain pair to estimate for.
5537
- * @returns Total fee estimate rounded up in the asset's base units.
5538
- */
5539
- async estimateInflowFee(request) {
5540
- if (isEthStablecoin(request.asset, request.chain)) {
5541
- const result = await createDepositAccountsActor(
5542
- this.canisterContext
5543
- ).estimate_deposit_fee([getEthStablecoinContractAddress(request.asset)]);
5544
- if ("Err" in result) {
5545
- throw mapDepositAccountErrorToLiquidiumError(result.Err);
5546
- }
5547
- return {
5548
- totalFee: roundInflowFeeEstimate(request, result.Ok)
5446
+ const lendingActor = createLendingActor(this.canisterContext);
5447
+ try {
5448
+ const expiryTimestamp = computeExpiryTimestampFromNow();
5449
+ const nonce = await lendingActor.get_nonce(signerAccount);
5450
+ const withdrawRequestData = {
5451
+ profileId: request.profileId,
5452
+ poolId: request.poolId,
5453
+ amount: request.amount,
5454
+ receiverAddress,
5455
+ signerWalletAddress: signerAccount,
5456
+ expiryTimestamp
5549
5457
  };
5550
- }
5551
- if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
5552
- const estimate = await this.estimateBtcInflowFee();
5553
5458
  return {
5554
- totalFee: roundInflowFeeEstimate(request, estimate.totalFee)
5459
+ kind: WalletActionKind.createWithdraw,
5460
+ executionKind: WalletExecutionKind.signMessage,
5461
+ actionType: WalletActionKind.createWithdraw,
5462
+ transferMode: TransferMode.native,
5463
+ account: signerAccount,
5464
+ message: createWithdrawAssetMessage(
5465
+ {
5466
+ pool_id: request.poolId,
5467
+ amount: request.amount.toString(),
5468
+ account: { type: "External", data: receiverAddress },
5469
+ expiry_timestamp: expiryTimestamp
5470
+ },
5471
+ nonce
5472
+ ),
5473
+ data: withdrawRequestData,
5474
+ submit: async (signatureInfo) => {
5475
+ return await this.submitWithdraw(withdrawRequestData, signatureInfo);
5476
+ }
5555
5477
  };
5478
+ } catch (error) {
5479
+ if (error instanceof LiquidiumError) {
5480
+ throw error;
5481
+ }
5482
+ throw mapCanisterCallErrorToLiquidiumError("get_nonce", error);
5556
5483
  }
5557
- throw new LiquidiumError(
5558
- LiquidiumErrorCode.VALIDATION_ERROR,
5559
- `Inflow fee estimates are not supported for ${request.asset} on ${request.chain}`
5560
- );
5561
- }
5562
- async estimateBtcInflowFee() {
5563
- const [minterFee, ledgerFee] = await Promise.all([
5564
- createCkBtcMinterActor(this.canisterContext).get_deposit_fee(),
5565
- createCkBtcLedgerActor(this.canisterContext).icrc1_fee()
5566
- ]);
5567
- return { totalFee: minterFee + ledgerFee };
5568
5484
  }
5569
- async sendAndSubmitNativeSupplyInflow(params) {
5570
- const { request, instruction, defaultSubmitInflowRequest } = params;
5571
- if (instruction.target.type !== "nativeAddress") {
5572
- throw new LiquidiumError(
5573
- LiquidiumErrorCode.VALIDATION_ERROR,
5574
- "Wallet-executed supply requires a native-address target"
5575
- );
5576
- }
5577
- if (!request.walletAdapter) {
5578
- throw new LiquidiumError(
5579
- LiquidiumErrorCode.VALIDATION_ERROR,
5580
- "Wallet-executed supply requires a wallet adapter"
5581
- );
5582
- }
5583
- const account = request.account?.trim();
5584
- if (!account) {
5585
- throw new LiquidiumError(
5586
- LiquidiumErrorCode.VALIDATION_ERROR,
5587
- "Wallet-executed transfer supply requires an account"
5588
- );
5589
- }
5590
- if (!request.amount || request.amount <= 0n) {
5591
- throw new LiquidiumError(
5592
- LiquidiumErrorCode.VALIDATION_ERROR,
5593
- "Wallet-executed supply requires a positive amount"
5485
+ async submitWithdraw(request, signatureInfo) {
5486
+ try {
5487
+ const result = await createLendingActor(this.canisterContext).withdraw(
5488
+ principal.Principal.fromText(request.profileId),
5489
+ {
5490
+ data: {
5491
+ expiry_timestamp: request.expiryTimestamp,
5492
+ account: { External: request.receiverAddress },
5493
+ pool_id: principal.Principal.fromText(request.poolId),
5494
+ amount: request.amount
5495
+ },
5496
+ signature_info: {
5497
+ Wallet: {
5498
+ signature: normalizeWalletSignature(
5499
+ signatureInfo.signature,
5500
+ signatureInfo.chain
5501
+ ),
5502
+ chain: mapWalletChainToLendingChain(signatureInfo.chain),
5503
+ account: request.signerWalletAddress
5504
+ }
5505
+ }
5506
+ }
5594
5507
  );
5508
+ if ("Err" in result) {
5509
+ throw mapLendingProtocolErrorToLiquidiumError(result.Err);
5510
+ }
5511
+ return mapExpectedOutflowDetails(
5512
+ mapCanisterOutflowDetails(result.Ok),
5513
+ OutflowType.withdrawal,
5514
+ "withdraw"
5515
+ );
5516
+ } catch (error) {
5517
+ if (error instanceof LiquidiumError) {
5518
+ throw error;
5519
+ }
5520
+ throw mapCanisterCallErrorToLiquidiumError("withdraw", error);
5595
5521
  }
5596
- const txid = await this.sendNativeSupplyTransaction({
5597
- walletAdapter: request.walletAdapter,
5598
- chain: instruction.chain,
5599
- toAddress: instruction.target.address,
5600
- amount: request.amount,
5601
- senderAccount: account,
5602
- asset: instruction.asset,
5603
- action: request.action
5604
- });
5605
- if (shouldSubmitInflow({ instruction, mechanism: SupplyPlanType.transfer })) {
5606
- await this.submitInflowWithRetry(txid, defaultSubmitInflowRequest);
5607
- }
5608
- return txid;
5609
5522
  }
5610
- async submitSupplyFlowInflow(params) {
5611
- if (!shouldSubmitInflow(params)) {
5612
- return { success: true, txid: params.submitRequest.txid };
5613
- }
5614
- return await this.submitInflow({
5615
- ...params.defaultSubmitInflowRequest,
5616
- ...params.submitRequest
5617
- });
5523
+ /**
5524
+ * Creates a withdraw outflow using the provided wallet adapter.
5525
+ *
5526
+ * This is the convenience form of `prepareWithdraw(...)` plus execution.
5527
+ *
5528
+ * @param params - Withdraw request fields plus `signerChain` and `signerWalletAdapter`.
5529
+ * @returns The canister {@link OutflowDetails} for the completed withdraw.
5530
+ */
5531
+ async withdraw(params) {
5532
+ const action = await this.prepareWithdraw(params);
5533
+ return await executeWith({
5534
+ walletAdapter: params.signerWalletAdapter,
5535
+ chain: params.signerChain,
5536
+ account: action.account
5537
+ })(action);
5618
5538
  }
5619
- async executeContractSupply(params) {
5620
- const { request, instruction, defaultSubmitInflowRequest } = params;
5621
- if (instruction.target.type !== "icrcAccount" || instruction.chain !== Chain.ETH) {
5539
+ /**
5540
+ * Prepares a borrow action that can be signed and submitted later.
5541
+ *
5542
+ * Use this when you need explicit control over signing and submission.
5543
+ *
5544
+ * @param request - Profile, pool, amount (borrow asset base units), outflow address, and signer wallet.
5545
+ * @returns A signable {@link BorrowAction} with `submit` wired to the canister.
5546
+ */
5547
+ async prepareBorrow(request) {
5548
+ const destinationAccount = request.receiverAddress.trim();
5549
+ const signerAccount = normalizeEvmAddress(
5550
+ request.signerWalletAddress.trim()
5551
+ );
5552
+ if (!destinationAccount) {
5622
5553
  throw new LiquidiumError(
5623
5554
  LiquidiumErrorCode.VALIDATION_ERROR,
5624
- "Contract-interaction supply is only supported for ETH ICRC-account targets"
5555
+ "Borrow requires a custom outflow account"
5625
5556
  );
5626
5557
  }
5627
- const walletAddressInput = request.account?.trim();
5628
- if (!walletAddressInput) {
5558
+ if (!signerAccount) {
5629
5559
  throw new LiquidiumError(
5630
5560
  LiquidiumErrorCode.VALIDATION_ERROR,
5631
- "Contract-interaction supply requires an account"
5561
+ "Borrow requires a signer account"
5632
5562
  );
5633
5563
  }
5634
- const walletAddress = normalizeAndValidateEvmAddress(
5635
- walletAddressInput,
5636
- "Invalid EVM wallet address"
5637
- );
5638
- if (!request.amount || request.amount <= 0n) {
5564
+ if (request.amount <= 0n) {
5639
5565
  throw new LiquidiumError(
5640
5566
  LiquidiumErrorCode.VALIDATION_ERROR,
5641
- "Contract-interaction supply requires a positive amount"
5567
+ "Borrow amount must be greater than 0"
5642
5568
  );
5643
5569
  }
5644
- const walletAdapter = request.walletAdapter;
5645
- if (!walletAdapter?.sendEthTransaction) {
5570
+ const selectedPool = await this.getPoolById(request.poolId);
5571
+ const selectedAsset = selectedPool.asset;
5572
+ const minimumBorrowAmountError = getBorrowAmountMinimumValidationError({
5573
+ amount: request.amount,
5574
+ asset: selectedAsset
5575
+ });
5576
+ if (minimumBorrowAmountError) {
5646
5577
  throw new LiquidiumError(
5647
5578
  LiquidiumErrorCode.VALIDATION_ERROR,
5648
- "Contract-interaction supply requires an ETH wallet adapter"
5579
+ minimumBorrowAmountError.message
5649
5580
  );
5650
5581
  }
5651
- this.requireApi();
5652
- this.requireEvmReadClient(
5653
- "Contract-interaction supply requires an EVM RPC URL or public client"
5654
- );
5655
- const evmSupplyContext = await this.getEvmSupplyContextForPool({
5656
- request: {
5582
+ const receiverAddress = normalizeExternalAddress({
5583
+ address: destinationAccount,
5584
+ asset: selectedAsset,
5585
+ chain: selectedPool.chain
5586
+ });
5587
+ const lendingActor = createLendingActor(this.canisterContext);
5588
+ try {
5589
+ const expiryTimestamp = computeExpiryTimestampFromNow();
5590
+ const nonce = await lendingActor.get_nonce(signerAccount);
5591
+ const borrowRequestData = {
5657
5592
  profileId: request.profileId,
5658
5593
  poolId: request.poolId,
5659
- walletAddress,
5660
5594
  amount: request.amount,
5661
- action: request.action
5662
- },
5663
- asset: instruction.asset,
5664
- chain: instruction.chain
5665
- });
5666
- const supplyAmount = BigInt(evmSupplyContext.amount);
5667
- if (BigInt(evmSupplyContext.balance) < supplyAmount) {
5668
- throw new LiquidiumError(
5669
- LiquidiumErrorCode.INSUFFICIENT_FUNDS,
5670
- `Insufficient ${evmSupplyContext.asset} balance for ${request.action}`
5671
- );
5595
+ receiverAddress,
5596
+ signerWalletAddress: signerAccount,
5597
+ expiryTimestamp
5598
+ };
5599
+ return {
5600
+ kind: WalletActionKind.createBorrow,
5601
+ executionKind: WalletExecutionKind.signMessage,
5602
+ actionType: WalletActionKind.createBorrow,
5603
+ transferMode: TransferMode.native,
5604
+ account: signerAccount,
5605
+ message: createBorrowAssetMessage(
5606
+ {
5607
+ pool_id: request.poolId,
5608
+ amount: request.amount.toString(),
5609
+ account: { type: "External", data: receiverAddress },
5610
+ expiry_timestamp: expiryTimestamp
5611
+ },
5612
+ nonce
5613
+ ),
5614
+ data: borrowRequestData,
5615
+ submit: async (signatureInfo) => {
5616
+ return await this.submitBorrow(borrowRequestData, signatureInfo);
5617
+ }
5618
+ };
5619
+ } catch (error) {
5620
+ if (error instanceof LiquidiumError) {
5621
+ throw error;
5622
+ }
5623
+ throw mapCanisterCallErrorToLiquidiumError("get_nonce", error);
5672
5624
  }
5673
- if (evmSupplyContext.approvalStrategy === EvmSupplyApprovalStrategy.resetThenApproveMax) {
5674
- await this.sendEthContractTransaction(
5675
- walletAdapter,
5676
- walletAddress,
5677
- createApproveTransaction({
5678
- tokenAddress: evmSupplyContext.tokenAddress,
5679
- spenderAddress: evmSupplyContext.spenderAddress,
5680
- amount: 0n
5681
- }),
5682
- `supply-${request.action}-approve-reset`
5683
- );
5684
- await this.waitForExpectedAllowance({
5685
- walletAddress,
5686
- tokenAddress: evmSupplyContext.tokenAddress,
5687
- spenderAddress: evmSupplyContext.spenderAddress,
5688
- amount: supplyAmount,
5689
- expectation: "zero"
5625
+ }
5626
+ async submitBorrow(request, signatureInfo) {
5627
+ try {
5628
+ const result = await createLendingActor(
5629
+ this.canisterContext
5630
+ ).borrow_assets(principal.Principal.fromText(request.profileId), {
5631
+ data: {
5632
+ expiry_timestamp: request.expiryTimestamp,
5633
+ account: { External: request.receiverAddress },
5634
+ pool_id: principal.Principal.fromText(request.poolId),
5635
+ amount: request.amount
5636
+ },
5637
+ signature_info: {
5638
+ Wallet: {
5639
+ signature: normalizeWalletSignature(
5640
+ signatureInfo.signature,
5641
+ signatureInfo.chain
5642
+ ),
5643
+ chain: mapWalletChainToLendingChain(signatureInfo.chain),
5644
+ account: request.signerWalletAddress
5645
+ }
5646
+ }
5690
5647
  });
5691
- }
5692
- if (evmSupplyContext.approvalStrategy !== EvmSupplyApprovalStrategy.none) {
5693
- await this.sendEthContractTransaction(
5694
- walletAdapter,
5695
- walletAddress,
5696
- createApproveTransaction({
5697
- tokenAddress: evmSupplyContext.tokenAddress,
5698
- spenderAddress: evmSupplyContext.spenderAddress,
5699
- amount: MAX_UINT256
5700
- }),
5701
- `supply-${request.action}-approve-max`
5648
+ if ("Err" in result) {
5649
+ throw mapLendingProtocolErrorToLiquidiumError(result.Err);
5650
+ }
5651
+ return mapExpectedOutflowDetails(
5652
+ mapCanisterOutflowDetails(result.Ok),
5653
+ OutflowType.borrow,
5654
+ "borrow_assets"
5655
+ );
5656
+ } catch (error) {
5657
+ if (error instanceof LiquidiumError) {
5658
+ throw error;
5659
+ }
5660
+ throw mapCanisterCallErrorToLiquidiumError("borrow_assets", error);
5661
+ }
5662
+ }
5663
+ /**
5664
+ * Creates a borrow outflow using the provided wallet adapter.
5665
+ *
5666
+ * This is the convenience form of `prepareBorrow(...)` plus execution.
5667
+ *
5668
+ * @param params - Borrow request fields plus `signerChain` and `signerWalletAdapter`.
5669
+ * @returns The lending canister receipt as {@link OutflowDetails}.
5670
+ *
5671
+ * @remarks
5672
+ * `id` is always present. `txid` may be missing on the first response; the SDK does not
5673
+ * poll for it. Use history or app-level polling if you need the chain transaction id.
5674
+ */
5675
+ async borrow(params) {
5676
+ const action = await this.prepareBorrow(params);
5677
+ return await executeWith({
5678
+ walletAdapter: params.signerWalletAdapter,
5679
+ chain: params.signerChain,
5680
+ account: action.account
5681
+ })(action);
5682
+ }
5683
+ /**
5684
+ * Resolves a supply target for a deposit or repayment and optionally broadcasts it.
5685
+ *
5686
+ * Transfer mode can return manual broadcast instructions when wallet fields are
5687
+ * omitted. Contract-interaction mode always requires `walletAdapter`, `account`,
5688
+ * and `amount` because it must prepare and submit approval/deposit calls.
5689
+ *
5690
+ * The SDK does not poll for inflow status. When a `txid` is returned, it is the
5691
+ * caller's responsibility to track confirmation state using their own polling.
5692
+ *
5693
+ * @returns A {@link SupplyFlow} receipt with `type`, `target`, `submit`, and
5694
+ * an optional `txid` present when the SDK broadcast for you.
5695
+ */
5696
+ async supply(request) {
5697
+ return await this.createSupplyFlowExecutor().create(request);
5698
+ }
5699
+ /**
5700
+ * Fetches ERC-20 supply planning data with the configured EVM read client.
5701
+ *
5702
+ * Requires `evmRpcUrl` or `evmPublicClient` on the client. Used internally by
5703
+ * contract-interaction `supply`.
5704
+ *
5705
+ * @param request - Profile, pool, wallet, amount (token base units), and action.
5706
+ * @returns Locally computed {@link EvmSupplyContext} for approvals and deposit.
5707
+ */
5708
+ async getEvmSupplyContext(request) {
5709
+ return await this.createSupplyFlowExecutor().getEvmSupplyContext(request);
5710
+ }
5711
+ /**
5712
+ * Returns the read-only deposit address for an ETH stablecoin inflow target.
5713
+ *
5714
+ * This is a query call that does not create or mutate state. Use it when you
5715
+ * need the deposit address without hitting the authorization-gated update path.
5716
+ *
5717
+ * @param request - Profile, pool, asset, and supply action.
5718
+ * @returns The EVM deposit address for the derived account.
5719
+ */
5720
+ async getDepositAddress(request) {
5721
+ if (!isEthStablecoin(request.asset, Chain.ETH)) {
5722
+ throw new LiquidiumError(
5723
+ LiquidiumErrorCode.VALIDATION_ERROR,
5724
+ "getDepositAddress is only supported for ETH stablecoins"
5702
5725
  );
5703
- await this.waitForExpectedAllowance({
5704
- walletAddress,
5705
- tokenAddress: evmSupplyContext.tokenAddress,
5706
- spenderAddress: evmSupplyContext.spenderAddress,
5707
- amount: supplyAmount,
5708
- expectation: "sufficient"
5709
- });
5710
5726
  }
5711
- const depositTxid = await this.sendEthContractTransaction(
5712
- walletAdapter,
5713
- walletAddress,
5714
- createDepositErc20Transaction({
5715
- depositContractAddress: evmSupplyContext.depositContractAddress,
5716
- tokenAddress: evmSupplyContext.tokenAddress,
5717
- amount: supplyAmount,
5718
- poolId: request.poolId,
5719
- profileId: request.profileId,
5720
- destinationAccount: instruction.target.account,
5721
- action: request.action
5722
- }),
5723
- `supply-${request.action}-deposit-erc20`
5727
+ const tokenAddress = getEthStablecoinContractAddress(request.asset);
5728
+ const subaccount = encodeInflowSubaccount({
5729
+ action: request.action,
5730
+ principal: principal.Principal.fromText(request.profileId)
5731
+ });
5732
+ const result = await createDepositAccountsActor(
5733
+ this.canisterContext
5734
+ ).get_deposit_address(
5735
+ {
5736
+ owner: principal.Principal.fromText(request.poolId),
5737
+ subaccount: [subaccount]
5738
+ },
5739
+ [tokenAddress]
5724
5740
  );
5725
- try {
5726
- await this.submitInflowWithRetry(depositTxid, defaultSubmitInflowRequest);
5727
- } catch {
5741
+ if ("Err" in result) {
5742
+ throw mapDepositAccountErrorToLiquidiumError(result.Err);
5728
5743
  }
5729
- return depositTxid;
5744
+ return result.Ok;
5730
5745
  }
5731
- async sendNativeSupplyTransaction(params) {
5732
- switch (params.chain) {
5733
- case Chain.BTC: {
5734
- if (!params.walletAdapter.sendBtcTransaction) {
5735
- throw new LiquidiumError(
5736
- LiquidiumErrorCode.VALIDATION_ERROR,
5737
- "BTC wallet adapter does not support transaction sending"
5738
- );
5739
- }
5740
- return await params.walletAdapter.sendBtcTransaction({
5741
- chain: Chain.BTC,
5742
- toAddress: params.toAddress,
5743
- amountSats: params.amount,
5744
- account: params.senderAccount,
5745
- actionType: `supply-${params.action}`,
5746
- transferMode: TransferMode.native
5747
- });
5748
- }
5749
- case Chain.ETH: {
5750
- if (!params.walletAdapter.sendEthTransaction) {
5751
- throw new LiquidiumError(
5752
- LiquidiumErrorCode.VALIDATION_ERROR,
5753
- "ETH wallet adapter does not support transaction sending"
5754
- );
5755
- }
5756
- if (isEthStablecoin(params.asset, params.chain)) {
5757
- return await params.walletAdapter.sendEthTransaction({
5758
- chain: Chain.ETH,
5759
- account: params.senderAccount,
5760
- actionType: `supply-${params.action}`,
5761
- transferMode: TransferMode.native,
5762
- transaction: createTransferErc20Transaction({
5763
- tokenAddress: getEthStablecoinContractAddress(params.asset),
5764
- recipientAddress: params.toAddress,
5765
- amount: params.amount
5766
- })
5767
- });
5768
- }
5769
- return await params.walletAdapter.sendEthTransaction({
5770
- chain: Chain.ETH,
5771
- account: params.senderAccount,
5772
- actionType: `supply-${params.action}`,
5773
- transferMode: TransferMode.native,
5774
- transaction: {
5775
- to: params.toAddress,
5776
- value: params.amount.toString()
5777
- }
5778
- });
5746
+ /**
5747
+ * Estimates the network/deposit fee for an inflow target.
5748
+ *
5749
+ * ETH stablecoin deposit-address estimates are served by the deposit-address
5750
+ * canister. BTC estimates include the ckBTC minter deposit fee and ledger fee.
5751
+ *
5752
+ * @param request - Asset and chain pair to estimate for.
5753
+ * @returns Total fee estimate rounded up in the asset's base units.
5754
+ */
5755
+ async estimateInflowFee(request) {
5756
+ if (isEthStablecoin(request.asset, request.chain)) {
5757
+ const result = await createDepositAccountsActor(
5758
+ this.canisterContext
5759
+ ).estimate_deposit_fee([getEthStablecoinContractAddress(request.asset)]);
5760
+ if ("Err" in result) {
5761
+ throw mapDepositAccountErrorToLiquidiumError(result.Err);
5779
5762
  }
5780
- default:
5781
- throw new LiquidiumError(
5782
- LiquidiumErrorCode.VALIDATION_ERROR,
5783
- `Native-address wallet execution is not supported for ${params.chain}`
5784
- );
5763
+ return {
5764
+ totalFee: roundInflowFeeEstimate(request, result.Ok)
5765
+ };
5766
+ }
5767
+ if (request.asset === Asset.BTC && request.chain === Chain.BTC) {
5768
+ const estimate = await this.estimateBtcInflowFee();
5769
+ return {
5770
+ totalFee: roundInflowFeeEstimate(request, estimate.totalFee)
5771
+ };
5785
5772
  }
5773
+ throw new LiquidiumError(
5774
+ LiquidiumErrorCode.VALIDATION_ERROR,
5775
+ `Inflow fee estimates are not supported for ${request.asset} on ${request.chain}`
5776
+ );
5786
5777
  }
5787
- async submitInflowWithRetry(txid, extraRequest) {
5788
- await retryWithBackoff({
5789
- execute: async () => {
5790
- await this.submitInflow({ txid, ...extraRequest });
5791
- },
5792
- maxAttempts: SUBMIT_INFLOW_MAX_ATTEMPTS,
5793
- initialRetryDelayMs: SUBMIT_INFLOW_INITIAL_RETRY_DELAY_MS,
5794
- backoffMultiplier: SUBMIT_INFLOW_RETRY_BACKOFF_MULTIPLIER,
5795
- shouldRetryError: isRetriableInflowSubmitError
5796
- });
5778
+ async estimateBtcInflowFee() {
5779
+ const [minterFee, ledgerFee] = await Promise.all([
5780
+ createCkBtcMinterActor(this.canisterContext).get_deposit_fee(),
5781
+ createCkBtcLedgerActor(this.canisterContext).icrc1_fee()
5782
+ ]);
5783
+ return { totalFee: minterFee + ledgerFee };
5797
5784
  }
5798
5785
  /**
5799
5786
  * Submits an inflow transaction id for faster indexing.
5800
5787
  *
5801
5788
  * Uses the Liquidium SDK API.
5802
5789
  *
5803
- * @param request - Broadcast `txid` plus optional `chain` and inflow `type`.
5790
+ * @param request - Broadcast `txid` plus inflow `operation` and optional `chain`.
5804
5791
  * @returns Acknowledgement including the submitted `txid`.
5805
5792
  */
5806
5793
  async submitInflow(request) {
5807
5794
  const apiClient = this.requireApi();
5808
- return await apiClient.post(
5809
- SdkApiPath.inflow,
5810
- request
5811
- );
5795
+ const response = await apiClient.post(SdkApiPath.inflow, request);
5796
+ return {
5797
+ txid: response.txid
5798
+ };
5812
5799
  }
5813
5800
  /**
5814
5801
  * Returns whether borrowing is currently disabled by the protocol.
@@ -5839,11 +5826,16 @@ var LendingModule = class {
5839
5826
  }
5840
5827
  return this.apiClient;
5841
5828
  }
5842
- requireEvmReadClient(message) {
5843
- if (!this.evmReadClient) {
5844
- throw new LiquidiumError(LiquidiumErrorCode.VALIDATION_ERROR, message);
5845
- }
5846
- return this.evmReadClient;
5829
+ createSupplyFlowExecutor() {
5830
+ return new SupplyFlowExecutor({
5831
+ canisterContext: this.canisterContext,
5832
+ evmReadClient: this.evmReadClient,
5833
+ getPoolById: async (poolId) => await this.getPoolById(poolId),
5834
+ requireApi: () => {
5835
+ this.requireApi();
5836
+ },
5837
+ submitInflow: async (request) => await this.submitInflow(request)
5838
+ });
5847
5839
  }
5848
5840
  async getPoolById(poolId) {
5849
5841
  const pools = await createFlexibleLendingActor(
@@ -5869,80 +5861,7 @@ var LendingModule = class {
5869
5861
  chain: selectedPool.chain
5870
5862
  });
5871
5863
  }
5872
- async sendEthContractTransaction(walletAdapter, walletAddress, request, actionType) {
5873
- if (!walletAdapter.sendEthTransaction) {
5874
- throw new LiquidiumError(
5875
- LiquidiumErrorCode.VALIDATION_ERROR,
5876
- "ETH wallet adapter does not support transaction sending"
5877
- );
5878
- }
5879
- return await walletAdapter.sendEthTransaction({
5880
- chain: Chain.ETH,
5881
- account: walletAddress,
5882
- actionType,
5883
- transferMode: TransferMode.native,
5884
- transaction: request
5885
- });
5886
- }
5887
- async waitForExpectedAllowance(params) {
5888
- const evmReadClient = this.requireEvmReadClient(
5889
- "Contract-interaction supply requires an EVM RPC URL or public client"
5890
- );
5891
- let lastPollingError;
5892
- for (let pollIndex = 0; pollIndex < ETH_APPROVAL_MAX_POLLS; pollIndex += 1) {
5893
- try {
5894
- const allowance = await readErc20Allowance({
5895
- evmReadClient,
5896
- tokenAddress: params.tokenAddress,
5897
- ownerAddress: params.walletAddress,
5898
- spenderAddress: params.spenderAddress
5899
- });
5900
- const matchesExpectation = params.expectation === "zero" ? allowance === 0n : allowance >= params.amount;
5901
- if (matchesExpectation) {
5902
- return;
5903
- }
5904
- } catch (error) {
5905
- lastPollingError = error;
5906
- }
5907
- await delay(ETH_APPROVAL_POLL_INTERVAL_MS);
5908
- }
5909
- const lastPollingErrorMessage = getUnknownErrorMessage(lastPollingError);
5910
- throw new LiquidiumError(
5911
- LiquidiumErrorCode.SERVICE_UNAVAILABLE,
5912
- lastPollingErrorMessage ? `Timed out waiting for ${params.expectation} ETH allowance update. Last polling error: ${lastPollingErrorMessage}` : `Timed out waiting for ${params.expectation} ETH allowance update`
5913
- );
5914
- }
5915
5864
  };
5916
- async function readErc20Allowance(params) {
5917
- const allowance = await params.evmReadClient.readContract({
5918
- address: params.tokenAddress,
5919
- abi: ERC20_ABI,
5920
- functionName: "allowance",
5921
- args: [
5922
- params.ownerAddress,
5923
- params.spenderAddress
5924
- ]
5925
- });
5926
- return BigInt(allowance);
5927
- }
5928
- async function readErc20Balance(params) {
5929
- const balance = await params.evmReadClient.readContract({
5930
- address: params.tokenAddress,
5931
- abi: ERC20_ABI,
5932
- functionName: "balanceOf",
5933
- args: [params.ownerAddress]
5934
- });
5935
- return BigInt(balance);
5936
- }
5937
- function getApprovalStrategy(params) {
5938
- if (params.allowance >= params.amount) {
5939
- return EvmSupplyApprovalStrategy.none;
5940
- }
5941
- if (params.allowance === 0n) {
5942
- return EvmSupplyApprovalStrategy.approveMax;
5943
- }
5944
- return EvmSupplyApprovalStrategy.resetThenApproveMax;
5945
- }
5946
5865
  function mapExpectedOutflowDetails(details, expectedOutflowType, operation) {
5947
5866
  if (details.outflowType !== expectedOutflowType) {
5948
5867
  throw new LiquidiumError(
@@ -5950,42 +5869,22 @@ function mapExpectedOutflowDetails(details, expectedOutflowType, operation) {
5950
5869
  `${operation} returned unexpected outflow type ${details.outflowType}`
5951
5870
  );
5952
5871
  }
5953
- return details;
5954
- }
5955
- async function delay(timeoutMs) {
5956
- await new Promise((resolve) => setTimeout(resolve, timeoutMs));
5957
- }
5958
- function isRetriableInflowSubmitError(error) {
5959
- if (!(error instanceof LiquidiumError)) {
5960
- return false;
5961
- }
5962
- return error.code === LiquidiumErrorCode.SERVICE_UNAVAILABLE;
5963
- }
5964
- function getUnknownErrorMessage(error) {
5965
- if (error instanceof Error) {
5966
- return error.message;
5967
- }
5968
- if (typeof error === "string") {
5969
- return error;
5970
- }
5971
- return null;
5972
- }
5973
- function getDefaultSubmitInflowRequest(params) {
5974
- if (params.chain !== Chain.BTC && params.chain !== Chain.ETH) {
5975
- return void 0;
5976
- }
5977
5872
  return {
5978
- chain: params.chain,
5979
- type: params.action === SupplyAction.repayment ? InflowSubmitType.REPAY : InflowSubmitType.DEPOSIT
5873
+ ...details,
5874
+ status: createLiquidiumStatus({
5875
+ operation: mapOutflowTypeToStatusOperation(expectedOutflowType),
5876
+ state: details.txid ? "confirming" : "processing"
5877
+ })
5980
5878
  };
5981
5879
  }
5982
- function shouldSubmitInflow(params) {
5983
- if (params.mechanism !== SupplyPlanType.transfer) {
5984
- return true;
5985
- }
5986
- return !isEthStablecoin(params.instruction.asset, params.instruction.chain);
5880
+ function mapOutflowTypeToStatusOperation(outflowType) {
5881
+ return outflowType;
5987
5882
  }
5988
5883
 
5884
+ // src/core/rates.ts
5885
+ var RATE_SCALE2 = 1000000000000000000000000000n;
5886
+ var RATE_DECIMALS = BigInt(RATE_SCALE2.toString().length - 1);
5887
+
5989
5888
  // src/modules/market/mappers.ts
5990
5889
  var DECIMAL_BASE = 10;
5991
5890
  var PAIR_SEPARATOR = "_";
@@ -6064,16 +5963,18 @@ var MarketModule = class {
6064
5963
  canisterContext;
6065
5964
  apiClient;
6066
5965
  /**
6067
- * Lists all pools with their current rates.
5966
+ * Lists SDK-supported pools with their current rates.
6068
5967
  *
6069
- * @returns All configured lending pools enriched with their current rate data.
5968
+ * Unsupported asset or chain variants returned by the canister are omitted.
5969
+ *
5970
+ * @returns Supported lending pools enriched with their current rate data.
6070
5971
  */
6071
5972
  async listPools() {
6072
5973
  void this.apiClient;
6073
5974
  try {
6074
5975
  const flexibleActor = createFlexibleLendingActor(this.canisterContext);
6075
5976
  const rawPools = await flexibleActor.list_pools();
6076
- const decodedPools = rawPools.map(decodeFlexiblePool).filter((pool) => pool !== null);
5977
+ const decodedPools = decodeSupportedFlexiblePools(rawPools);
6077
5978
  return await Promise.all(
6078
5979
  decodedPools.map(async (pool) => {
6079
5980
  const poolRate = await flexibleActor.get_pool_rate(pool.principal);
@@ -6180,6 +6081,17 @@ var MarketModule = class {
6180
6081
  }
6181
6082
  }
6182
6083
  };
6084
+ function decodeSupportedFlexiblePools(rawPools) {
6085
+ const decodedPools = [];
6086
+ for (const rawPool of rawPools) {
6087
+ const decodedPool = decodeFlexiblePool(rawPool);
6088
+ if (!decodedPool) {
6089
+ continue;
6090
+ }
6091
+ decodedPools.push(decodedPool);
6092
+ }
6093
+ return decodedPools;
6094
+ }
6183
6095
 
6184
6096
  // src/modules/positions/mappers.ts
6185
6097
  var USD_VALUE_SCALE_DECIMALS = 27n;
@@ -6510,6 +6422,7 @@ var LiquidiumClient = class {
6510
6422
  this.instantLoans = new InstantLoansModule(
6511
6423
  this.canisterContext,
6512
6424
  this.apiClient,
6425
+ this.activities,
6513
6426
  this.lending,
6514
6427
  this.positions
6515
6428
  );
@@ -6533,18 +6446,13 @@ function resolveEvmReadClient(config) {
6533
6446
 
6534
6447
  exports.AccountsModule = AccountsModule;
6535
6448
  exports.ActivitiesModule = ActivitiesModule;
6536
- exports.ActivityDirection = ActivityDirection;
6537
6449
  exports.ActivityFilter = ActivityFilter;
6538
- exports.ActivityKind = ActivityKind;
6539
- exports.ActivityStatus = ActivityStatus;
6540
6450
  exports.Asset = Asset;
6541
6451
  exports.CK_ETH_DEPOSIT_CONTRACT_ADDRESS = CK_ETH_DEPOSIT_CONTRACT_ADDRESS;
6542
6452
  exports.Chain = Chain;
6543
6453
  exports.Environment = Environment;
6544
6454
  exports.EvmSupplyApprovalStrategy = EvmSupplyApprovalStrategy;
6545
6455
  exports.HistoryModule = HistoryModule;
6546
- exports.InflowSubmitType = InflowSubmitType;
6547
- exports.InstantLoanStatus = InstantLoanStatus;
6548
6456
  exports.InstantLoansModule = InstantLoansModule;
6549
6457
  exports.LendingModule = LendingModule;
6550
6458
  exports.LiquidiumClient = LiquidiumClient;
@@ -6558,13 +6466,13 @@ exports.QuoteModule = QuoteModule;
6558
6466
  exports.QuoteValidationErrorCode = QuoteValidationErrorCode;
6559
6467
  exports.QuoteWarningCode = QuoteWarningCode;
6560
6468
  exports.RATE_DECIMALS = RATE_DECIMALS;
6561
- exports.RATE_SCALE = RATE_SCALE;
6469
+ exports.RATE_SCALE = RATE_SCALE2;
6562
6470
  exports.SupplyAction = SupplyAction;
6563
6471
  exports.SupplyPlanType = SupplyPlanType;
6564
6472
  exports.TransferMode = TransferMode;
6565
6473
  exports.USDC_CONTRACT_ADDRESS = USDC_CONTRACT_ADDRESS;
6566
6474
  exports.USDT_CONTRACT_ADDRESS = USDT_CONTRACT_ADDRESS;
6567
- exports.UserHistoryStatus = UserHistoryStatus;
6475
+ exports.WalletActionKind = WalletActionKind;
6568
6476
  exports.WalletExecutionKind = WalletExecutionKind;
6569
6477
  exports.createTransferErc20Transaction = createTransferErc20Transaction;
6570
6478
  exports.executeWith = executeWith;