@liquidium/client 0.7.0 → 0.8.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.d.cts CHANGED
@@ -1,8 +1,18 @@
1
- import { Identity, HttpAgent } from '@icp-sdk/core/agent';
1
+ import { Agent, Identity } from '@icp-sdk/core/agent';
2
2
  import { PublicClient } from 'viem';
3
3
 
4
- /** Minimal viem-compatible client shape required for SDK EVM read calls. */
5
- type EvmReadClient = Pick<PublicClient, "readContract">;
4
+ /**
5
+ * Minimal viem-compatible client shape required for SDK EVM read calls.
6
+ *
7
+ * When `getCode` is present and `chain` identifies Ethereum mainnet, the SDK
8
+ * uses it for a best-effort native ETH contract-destination check. Provider
9
+ * failures fail open and do not block the outflow.
10
+ */
11
+ interface EvmReadClient {
12
+ readContract: PublicClient["readContract"];
13
+ chain?: PublicClient["chain"];
14
+ getCode?: PublicClient["getCode"];
15
+ }
6
16
  /**
7
17
  * Runtime options for `new LiquidiumClient(config)`.
8
18
  *
@@ -12,6 +22,8 @@ type EvmReadClient = Pick<PublicClient, "readContract">;
12
22
  interface LiquidiumClientConfig {
13
23
  /** Preset canister IDs. Only `mainnet` is bundled. */
14
24
  environment?: Environment;
25
+ /** Preconfigured ICP agent. Takes precedence over `icHost` and `identity`. */
26
+ agent?: Agent;
15
27
  /** ICP replica host override (defaults follow `@icp-sdk/core/agent`). */
16
28
  icHost?: string;
17
29
  /** Agent identity for signed canister calls. */
@@ -30,11 +42,11 @@ interface LiquidiumClientConfig {
30
42
  fetch?: typeof fetch;
31
43
  /** Per-request timeout for SDK API calls in milliseconds. */
32
44
  timeoutMs?: number;
33
- /** Ethereum RPC URL used for public ERC-20 reads in EVM supply flows. */
45
+ /** Ethereum RPC URL used for best-effort native ETH outflow checks and public ERC-20 reads. */
34
46
  evmRpcUrl?: string;
35
47
  /** Optional headers for RPC providers that authenticate via HTTP headers. */
36
48
  evmRpcHeaders?: Record<string, string>;
37
- /** Existing viem public client or compatible read client for EVM reads. */
49
+ /** Existing viem client; mainnet `chain` and `getCode` enable native ETH outflow checks. */
38
50
  evmPublicClient?: EvmReadClient;
39
51
  }
40
52
  /** Pool canister principal text values grouped by pool asset. */
@@ -168,7 +180,7 @@ interface Wallet {
168
180
  }
169
181
 
170
182
  interface CanisterContext {
171
- agent: HttpAgent;
183
+ agent: Agent;
172
184
  canisterIds: CanisterIds;
173
185
  }
174
186
 
@@ -2305,6 +2317,7 @@ declare const LiquidiumErrorCode: {
2305
2317
  readonly WITHDRAW_TOO_LOW: "WITHDRAW_TOO_LOW";
2306
2318
  readonly REPAYMENT_EXCEEDS_DEBT: "REPAYMENT_EXCEEDS_DEBT";
2307
2319
  readonly INVALID_ADDRESS: "INVALID_ADDRESS";
2320
+ readonly CONTRACT_DESTINATION_UNSUPPORTED: "CONTRACT_DESTINATION_UNSUPPORTED";
2308
2321
  readonly DEPOSIT_ADDRESS_ERROR: "DEPOSIT_ADDRESS_ERROR";
2309
2322
  readonly NETWORK_ERROR: "NETWORK_ERROR";
2310
2323
  readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
package/dist/index.d.ts CHANGED
@@ -1,8 +1,18 @@
1
- import { Identity, HttpAgent } from '@icp-sdk/core/agent';
1
+ import { Agent, Identity } from '@icp-sdk/core/agent';
2
2
  import { PublicClient } from 'viem';
3
3
 
4
- /** Minimal viem-compatible client shape required for SDK EVM read calls. */
5
- type EvmReadClient = Pick<PublicClient, "readContract">;
4
+ /**
5
+ * Minimal viem-compatible client shape required for SDK EVM read calls.
6
+ *
7
+ * When `getCode` is present and `chain` identifies Ethereum mainnet, the SDK
8
+ * uses it for a best-effort native ETH contract-destination check. Provider
9
+ * failures fail open and do not block the outflow.
10
+ */
11
+ interface EvmReadClient {
12
+ readContract: PublicClient["readContract"];
13
+ chain?: PublicClient["chain"];
14
+ getCode?: PublicClient["getCode"];
15
+ }
6
16
  /**
7
17
  * Runtime options for `new LiquidiumClient(config)`.
8
18
  *
@@ -12,6 +22,8 @@ type EvmReadClient = Pick<PublicClient, "readContract">;
12
22
  interface LiquidiumClientConfig {
13
23
  /** Preset canister IDs. Only `mainnet` is bundled. */
14
24
  environment?: Environment;
25
+ /** Preconfigured ICP agent. Takes precedence over `icHost` and `identity`. */
26
+ agent?: Agent;
15
27
  /** ICP replica host override (defaults follow `@icp-sdk/core/agent`). */
16
28
  icHost?: string;
17
29
  /** Agent identity for signed canister calls. */
@@ -30,11 +42,11 @@ interface LiquidiumClientConfig {
30
42
  fetch?: typeof fetch;
31
43
  /** Per-request timeout for SDK API calls in milliseconds. */
32
44
  timeoutMs?: number;
33
- /** Ethereum RPC URL used for public ERC-20 reads in EVM supply flows. */
45
+ /** Ethereum RPC URL used for best-effort native ETH outflow checks and public ERC-20 reads. */
34
46
  evmRpcUrl?: string;
35
47
  /** Optional headers for RPC providers that authenticate via HTTP headers. */
36
48
  evmRpcHeaders?: Record<string, string>;
37
- /** Existing viem public client or compatible read client for EVM reads. */
49
+ /** Existing viem client; mainnet `chain` and `getCode` enable native ETH outflow checks. */
38
50
  evmPublicClient?: EvmReadClient;
39
51
  }
40
52
  /** Pool canister principal text values grouped by pool asset. */
@@ -168,7 +180,7 @@ interface Wallet {
168
180
  }
169
181
 
170
182
  interface CanisterContext {
171
- agent: HttpAgent;
183
+ agent: Agent;
172
184
  canisterIds: CanisterIds;
173
185
  }
174
186
 
@@ -2305,6 +2317,7 @@ declare const LiquidiumErrorCode: {
2305
2317
  readonly WITHDRAW_TOO_LOW: "WITHDRAW_TOO_LOW";
2306
2318
  readonly REPAYMENT_EXCEEDS_DEBT: "REPAYMENT_EXCEEDS_DEBT";
2307
2319
  readonly INVALID_ADDRESS: "INVALID_ADDRESS";
2320
+ readonly CONTRACT_DESTINATION_UNSUPPORTED: "CONTRACT_DESTINATION_UNSUPPORTED";
2308
2321
  readonly DEPOSIT_ADDRESS_ERROR: "DEPOSIT_ADDRESS_ERROR";
2309
2322
  readonly NETWORK_ERROR: "NETWORK_ERROR";
2310
2323
  readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ var LiquidiumErrorCode = {
45
45
  WITHDRAW_TOO_LOW: "WITHDRAW_TOO_LOW",
46
46
  REPAYMENT_EXCEEDS_DEBT: "REPAYMENT_EXCEEDS_DEBT",
47
47
  INVALID_ADDRESS: "INVALID_ADDRESS",
48
+ CONTRACT_DESTINATION_UNSUPPORTED: "CONTRACT_DESTINATION_UNSUPPORTED",
48
49
  DEPOSIT_ADDRESS_ERROR: "DEPOSIT_ADDRESS_ERROR",
49
50
  // Transport
50
51
  NETWORK_ERROR: "NETWORK_ERROR",
@@ -318,6 +319,12 @@ function appendErrorContext(message, context) {
318
319
  return `${message} (${details.join(", ")})`;
319
320
  }
320
321
  function createCanisterContext(opts) {
322
+ if (opts.agent) {
323
+ return {
324
+ agent: opts.agent,
325
+ canisterIds: opts.canisterIds
326
+ };
327
+ }
321
328
  const host = resolveHost(opts.icHost);
322
329
  const agent = HttpAgent.createSync({
323
330
  host,
@@ -1785,6 +1792,11 @@ var HISTORY_ACTIVITIES = `${SDK_API_V2_PATH}/history/activities`;
1785
1792
  var HISTORY_USERS = `${SDK_API_V2_PATH}/history/users`;
1786
1793
  var INFLOW = `${SDK_API_V2_PATH}/inflow`;
1787
1794
  var SIMPLE_LOANS = `${SDK_API_V1_PATH}/instant-loans`;
1795
+ function buildEthereumAddressBytecodePath(request) {
1796
+ return `${SDK_API_V2_PATH}/ethereum/addresses/${encodeURIComponent(
1797
+ request.address
1798
+ )}/bytecode`;
1799
+ }
1788
1800
  function buildHistoryUserTransactionsPath(profileId, query) {
1789
1801
  const base = `${HISTORY_USERS}/${encodeURIComponent(profileId)}/transactions`;
1790
1802
  const qs = query.toString();
@@ -3341,6 +3353,71 @@ function accountTypeToString(accountType) {
3341
3353
  return `Principal:${accountType.data}`;
3342
3354
  }
3343
3355
  }
3356
+ var EMPTY_EVM_BYTECODE = "0x";
3357
+ var LAST_ETHEREUM_MAINNET_PRECOMPILE_ADDRESS = 0x11n;
3358
+ var CONTRACT_DESTINATION_UNSUPPORTED_MESSAGE = "Contract addresses are not supported for native ETH withdrawals or borrowing";
3359
+ var RESERVED_DESTINATION_MESSAGE = "Ethereum outflow destination must not be the zero address or a precompile";
3360
+ async function guardEthereumOutflowDestination({
3361
+ address,
3362
+ apiClient,
3363
+ asset,
3364
+ chain,
3365
+ evmReadClient
3366
+ }) {
3367
+ if (chain !== Chain.ETH) {
3368
+ return;
3369
+ }
3370
+ const normalizedAddress = getAddress(address);
3371
+ if (BigInt(normalizedAddress) <= LAST_ETHEREUM_MAINNET_PRECOMPILE_ADDRESS) {
3372
+ throw new LiquidiumError(
3373
+ LiquidiumErrorCode.INVALID_ADDRESS,
3374
+ RESERVED_DESTINATION_MESSAGE
3375
+ );
3376
+ }
3377
+ if (asset !== Asset.ETH) {
3378
+ return;
3379
+ }
3380
+ if (evmReadClient?.chain?.id === mainnet.id && evmReadClient.getCode) {
3381
+ let bytecode;
3382
+ try {
3383
+ bytecode = await evmReadClient.getCode({
3384
+ address: normalizedAddress
3385
+ });
3386
+ } catch {
3387
+ return;
3388
+ }
3389
+ if (bytecode === void 0 || bytecode === EMPTY_EVM_BYTECODE) {
3390
+ return;
3391
+ }
3392
+ if (typeof bytecode !== "string" || !/^0x(?:[0-9a-fA-F]{2})*$/.test(bytecode)) {
3393
+ return;
3394
+ }
3395
+ throw new LiquidiumError(
3396
+ LiquidiumErrorCode.CONTRACT_DESTINATION_UNSUPPORTED,
3397
+ CONTRACT_DESTINATION_UNSUPPORTED_MESSAGE
3398
+ );
3399
+ }
3400
+ if (!apiClient) {
3401
+ return;
3402
+ }
3403
+ let response;
3404
+ try {
3405
+ response = await apiClient.get(
3406
+ buildEthereumAddressBytecodePath({ address: normalizedAddress })
3407
+ );
3408
+ } catch {
3409
+ return;
3410
+ }
3411
+ if (!response || typeof response !== "object" || !("hasDeployedBytecode" in response) || typeof response.hasDeployedBytecode !== "boolean") {
3412
+ return;
3413
+ }
3414
+ if (response.hasDeployedBytecode) {
3415
+ throw new LiquidiumError(
3416
+ LiquidiumErrorCode.CONTRACT_DESTINATION_UNSUPPORTED,
3417
+ CONTRACT_DESTINATION_UNSUPPORTED_MESSAGE
3418
+ );
3419
+ }
3420
+ }
3344
3421
 
3345
3422
  // src/core/pool-ledger-assets.ts
3346
3423
  function getPoolLedgerAssetRoute(params) {
@@ -4723,6 +4800,13 @@ var LendingModule = class {
4723
4800
  poolChain: selectedPool.chain,
4724
4801
  destinationChain: request.chain
4725
4802
  });
4803
+ await guardEthereumOutflowDestination({
4804
+ address: receiver.address,
4805
+ apiClient: this.apiClient,
4806
+ asset: selectedAsset,
4807
+ chain: request.chain,
4808
+ evmReadClient: this.evmReadClient
4809
+ });
4726
4810
  const lendingActor = createLendingActor(this.canisterContext);
4727
4811
  try {
4728
4812
  const expiryTimestamp = computeExpiryTimestampFromNow();
@@ -4741,7 +4825,9 @@ var LendingModule = class {
4741
4825
  };
4742
4826
  const withdrawSubmissionData = {
4743
4827
  ...withdrawActionData,
4744
- receiverAccount: receiver.canisterAccount
4828
+ asset: selectedAsset,
4829
+ receiverAccount: receiver.canisterAccount,
4830
+ receiverAddress: receiver.address
4745
4831
  };
4746
4832
  return {
4747
4833
  kind: WalletActionKind.createWithdraw,
@@ -4773,6 +4859,13 @@ var LendingModule = class {
4773
4859
  }
4774
4860
  }
4775
4861
  async submitWithdraw(request, signatureInfo) {
4862
+ await guardEthereumOutflowDestination({
4863
+ address: request.receiverAddress,
4864
+ apiClient: this.apiClient,
4865
+ asset: request.asset,
4866
+ chain: request.chain,
4867
+ evmReadClient: this.evmReadClient
4868
+ });
4776
4869
  try {
4777
4870
  const result = await createLendingActor(this.canisterContext).withdraw(
4778
4871
  Principal.fromText(request.profileId),
@@ -4877,6 +4970,13 @@ var LendingModule = class {
4877
4970
  poolChain: selectedPool.chain,
4878
4971
  destinationChain: request.chain
4879
4972
  });
4973
+ await guardEthereumOutflowDestination({
4974
+ address: receiver.address,
4975
+ apiClient: this.apiClient,
4976
+ asset: selectedAsset,
4977
+ chain: request.chain,
4978
+ evmReadClient: this.evmReadClient
4979
+ });
4880
4980
  await this.guardBorrowSameAssetPolicy({
4881
4981
  profileId: request.profileId,
4882
4982
  pool: selectedPool
@@ -4899,7 +4999,9 @@ var LendingModule = class {
4899
4999
  };
4900
5000
  const borrowSubmissionData = {
4901
5001
  ...borrowActionData,
4902
- receiverAccount: receiver.canisterAccount
5002
+ asset: selectedAsset,
5003
+ receiverAccount: receiver.canisterAccount,
5004
+ receiverAddress: receiver.address
4903
5005
  };
4904
5006
  return {
4905
5007
  kind: WalletActionKind.createBorrow,
@@ -4928,6 +5030,13 @@ var LendingModule = class {
4928
5030
  }
4929
5031
  }
4930
5032
  async submitBorrow(request, signatureInfo) {
5033
+ await guardEthereumOutflowDestination({
5034
+ address: request.receiverAddress,
5035
+ apiClient: this.apiClient,
5036
+ asset: request.asset,
5037
+ chain: request.chain,
5038
+ evmReadClient: this.evmReadClient
5039
+ });
4931
5040
  try {
4932
5041
  const result = await createLendingActor(
4933
5042
  this.canisterContext
@@ -7620,6 +7729,7 @@ var LiquidiumClient = class {
7620
7729
  const canisterIds = resolveCanisterIds(environment, config.canisterIds);
7621
7730
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7622
7731
  this.canisterContext = createCanisterContext({
7732
+ agent: config.agent,
7623
7733
  icHost: config.icHost,
7624
7734
  identity: config.identity,
7625
7735
  canisterIds