dhali-js 2.1.0 → 3.0.3

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.
@@ -4,7 +4,7 @@ const { sign: signClaim } = require("ripple-keypairs");
4
4
 
5
5
  const { fetchPublicConfig, notifyAdminGateway, retrieveChannelIdFromFirestoreRest } = require("./configUtils");
6
6
 
7
- class ChannelNotFound extends Error { }
7
+ const { ChannelNotFound } = require("./utils");
8
8
 
9
9
  /**
10
10
  * A management tool for generating payment claims for use with Dhali APIs (XRPL).
@@ -13,15 +13,13 @@ class DhaliXrplChannelManager {
13
13
  /**
14
14
  * @param {import("xrpl").Wallet} wallet
15
15
  * @param {import("xrpl").Client} rpc_client
16
- * @param {string} protocol
17
16
  * @param {import("./Currency")} currency
18
17
  * @param {typeof fetch} [httpClient]
19
18
  * @param {object} [public_config]
20
19
  */
21
- constructor(wallet, rpc_client, protocol, currency, httpClient = fetch, public_config) {
20
+ constructor(wallet, rpc_client, currency, httpClient = fetch, public_config) {
22
21
  this.wallet = wallet;
23
22
  this.rpc_client = rpc_client;
24
- this.protocol = protocol;
25
23
  this.currency = currency;
26
24
  this.httpClient = httpClient || fetch;
27
25
  this.public_config = public_config;
@@ -38,7 +36,7 @@ class DhaliXrplChannelManager {
38
36
 
39
37
  if (!this.destination) {
40
38
  try {
41
- this.destination = this.public_config.DHALI_PUBLIC_ADDRESSES[this.protocol][this.currency.code].wallet_id;
39
+ this.destination = this.public_config.DHALI_PUBLIC_ADDRESSES[this.currency.network][this.currency.code].wallet_id;
42
40
  } catch (e) {
43
41
  // Fallback to default if needed, or throw
44
42
  this.destination = "rJiAX3Xk2Fq3KJrjsGajrB5LENZq7VCwAd";
@@ -53,7 +51,7 @@ class DhaliXrplChannelManager {
53
51
  */
54
52
  async _retrieveChannelIdFromFirestore() {
55
53
  return await retrieveChannelIdFromFirestoreRest(
56
- this.protocol,
54
+ this.currency.network,
57
55
  this.currency,
58
56
  this.wallet.address,
59
57
  this.httpClient
@@ -147,7 +145,7 @@ class DhaliXrplChannelManager {
147
145
  currencyIdentifier = `${this.currency.code}.${this.currency.tokenAddress}`;
148
146
  }
149
147
  await notifyAdminGateway(
150
- this.protocol,
148
+ this.currency.network,
151
149
  currencyIdentifier,
152
150
  this.wallet.classicAddress,
153
151
  channelId,
@@ -185,7 +183,7 @@ class DhaliXrplChannelManager {
185
183
  const claim = {
186
184
  version: "2",
187
185
  account: this.wallet.classicAddress,
188
- protocol: this.protocol,
186
+ protocol: this.currency.network,
189
187
  currency: { code: "XRP", scale: 6 },
190
188
  destination_account: this.destination,
191
189
  authorized_to_claim: allowed.toString(),
@@ -8,11 +8,8 @@ const Currency = require("./Currency");
8
8
 
9
9
  /**
10
10
  * Fetches and parses available Dhali currencies and configurations.
11
- * @returns {Promise<Object.<string, Object.<string, NetworkCurrencyConfig>>>}
12
- */
13
- /**
14
11
  * @param {typeof fetch} [httpClient]
15
- * @returns {Promise<Object>}
12
+ * @returns {Promise<Currency[]>}
16
13
  */
17
14
  async function getAvailableDhaliCurrencies(httpClient = fetch) {
18
15
  const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
@@ -28,11 +25,10 @@ async function getAvailableDhaliCurrencies(httpClient = fetch) {
28
25
  }
29
26
 
30
27
  const publicAddresses = data.DHALI_PUBLIC_ADDRESSES || {};
31
- /** @type {Object.<string, Object.<string, NetworkCurrencyConfig>>} */
32
- const result = {};
28
+ /** @type {Currency[]} */
29
+ const result = [];
33
30
 
34
31
  for (const [network, currencies] of Object.entries(publicAddresses)) {
35
- result[network] = {};
36
32
  for (const [code, details] of Object.entries(currencies)) {
37
33
  const tokenAddress = details.issuer || null;
38
34
  const scale = details.scale || 6;
@@ -40,12 +36,8 @@ async function getAvailableDhaliCurrencies(httpClient = fetch) {
40
36
 
41
37
  if (!destination) continue;
42
38
 
43
- const curr = new Currency(code, scale, tokenAddress);
44
-
45
- result[network][code] = {
46
- currency: curr,
47
- destinationAddress: destination
48
- };
39
+ const curr = new Currency(network, code, scale, tokenAddress);
40
+ result.push(curr);
49
41
  }
50
42
  }
51
43
  return result;
@@ -39,6 +39,14 @@ function wrapAsX402PaymentPayload(claimBase64, paymentRequirementBase64) {
39
39
  return Buffer.from(JSON.stringify(x402Payload)).toString("base64");
40
40
  }
41
41
 
42
+ class ChannelNotFound extends Error {
43
+ constructor(message = "No open payment channel found.") {
44
+ super(message);
45
+ this.name = "ChannelNotFound";
46
+ }
47
+ }
48
+
42
49
  module.exports = {
43
50
  wrapAsX402PaymentPayload,
51
+ ChannelNotFound
44
52
  };
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { DhaliChannelManager } = require("./dhali/DhaliChannelManager");
2
2
  const { DhaliXrplChannelManager, ChannelNotFound } = require("./dhali/DhaliXrplChannelManager");
3
3
  const { DhaliEthChannelManager } = require("./dhali/DhaliEthChannelManager");
4
- const { Currency } = require("./dhali/Currency");
4
+ const Currency = require("./dhali/Currency");
5
5
  const { getAvailableDhaliCurrencies } = require("./dhali/configUtils");
6
6
  const { wrapAsX402PaymentPayload } = require("./dhali/utils");
7
7
 
@@ -53,11 +53,11 @@ describe("DhaliChannelManager", () => {
53
53
  submitAndWait: jest.fn(),
54
54
  autofill: jest.fn().mockResolvedValue({}),
55
55
  };
56
- currency = new Currency("XRP", 6);
56
+ currency = new Currency("XRPL.MAINNET", "XRP", 6);
57
57
 
58
58
  const mockHttp = jest.fn();
59
59
  configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("CHAN123");
60
- manager = new DhaliXrplChannelManager(wallet, mockClient, "XRPL.MAINNET", currency, mockHttp);
60
+ manager = new DhaliXrplChannelManager(wallet, mockClient, currency, mockHttp);
61
61
  });
62
62
 
63
63
  afterEach(() => {
@@ -82,7 +82,7 @@ describe("DhaliChannelManager", () => {
82
82
  test("throws ChannelNotFound if firestore returns null", async () => {
83
83
  const mockHttp = jest.fn();
84
84
  configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue(null);
85
- manager = new DhaliXrplChannelManager(wallet, mockClient, "XRPL.MAINNET", currency, mockHttp);
85
+ manager = new DhaliXrplChannelManager(wallet, mockClient, currency, mockHttp);
86
86
  await expect(manager.getAuthToken(100)).rejects.toThrow(ChannelNotFound);
87
87
  await expect(manager.getAuthToken(100)).rejects.toThrow(/No open payment channel from/);
88
88
  expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
@@ -147,4 +147,26 @@ describe("DhaliChannelManager", () => {
147
147
  );
148
148
  });
149
149
  });
150
+
151
+ describe("Casing", () => {
152
+ test("queries Firestore with original XRPL address casing", async () => {
153
+ const mockHttp = jest.fn();
154
+ wallet.address = "rMixedCaseAddress";
155
+ manager = new DhaliXrplChannelManager(
156
+ wallet,
157
+ mockClient,
158
+ currency,
159
+ mockHttp,
160
+ );
161
+
162
+ await manager._retrieveChannelIdFromFirestore();
163
+
164
+ expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
165
+ "XRPL.MAINNET",
166
+ currency,
167
+ "rMixedCaseAddress", // Should NOT be lower-cased
168
+ mockHttp,
169
+ );
170
+ });
171
+ });
150
172
  });
@@ -1,27 +1,32 @@
1
1
  const { DhaliEthChannelManager } = require("../src/dhali/DhaliEthChannelManager");
2
2
  const Currency = require("../src/dhali/Currency");
3
- const { ethers } = require("ethers");
3
+ const { keccak256, encodeAbiParameters, parseAbiParameters } = require("viem");
4
4
  const configUtils = require("../src/dhali/configUtils");
5
+ const { ChannelNotFound } = require("../src/dhali/utils");
5
6
 
6
7
  jest.mock("../src/dhali/configUtils");
7
8
 
8
9
  describe("DhaliEthChannelManager", () => {
9
- let mockSigner;
10
- let mockProvider;
10
+ let mockWalletClient;
11
+ let mockPublicClient;
11
12
  let currency;
12
13
  let publicConfig;
13
14
  let manager;
14
15
 
15
16
  beforeEach(() => {
16
- mockSigner = {
17
- getAddress: jest.fn().mockResolvedValue("0x0000000000000000000000000000000000000001"),
18
- getNonce: jest.fn(),
19
- estimateGas: jest.fn(),
20
- signTypedData: jest.fn(),
21
- sendTransaction: jest.fn()
17
+ mockWalletClient = {
18
+ getAddresses: jest.fn().mockResolvedValue(["0x0000000000000000000000000000000000000001"]),
19
+ sendTransaction: jest.fn(),
20
+ signTypedData: jest.fn()
22
21
  };
23
- mockProvider = {};
24
- currency = new Currency("ETH", 18);
22
+ mockPublicClient = {
23
+ getGasPrice: jest.fn().mockResolvedValue(BigInt(1000000000)),
24
+ estimateGas: jest.fn().mockResolvedValue(BigInt(21000)),
25
+ getTransactionCount: jest.fn().mockResolvedValue(10),
26
+ waitForTransactionReceipt: jest.fn(),
27
+ call: jest.fn()
28
+ };
29
+ currency = new Currency("ETHEREUM", "ETH", 18);
25
30
  publicConfig = {
26
31
  DHALI_PUBLIC_ADDRESSES: {
27
32
  ETHEREUM: {
@@ -33,30 +38,30 @@ describe("DhaliEthChannelManager", () => {
33
38
  }
34
39
  };
35
40
  configUtils.fetchPublicConfig.mockResolvedValue(publicConfig);
36
- manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
41
+ manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null, publicConfig);
37
42
  jest.clearAllMocks();
38
43
  });
39
44
 
40
45
  test("initializes without default http client", () => {
41
- const localManager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
46
+ const localManager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null, publicConfig);
42
47
  expect(localManager.httpClient).toBe(fetch);
43
48
  expect(localManager.chainId).toBe(1);
44
49
  });
45
50
 
46
51
  test("initializes with provided http client", () => {
47
52
  const mockHttp = jest.fn();
48
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
53
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
49
54
  expect(manager.httpClient).toBe(mockHttp);
50
55
  });
51
56
 
52
57
  test("initializes without config or addresses (lazy resolution)", () => {
53
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null);
58
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null);
54
59
  expect(manager.destinationAddress).toBeUndefined();
55
60
  expect(manager.contractAddress).toBeUndefined();
56
61
  });
57
62
 
58
63
  test("resolves addresses lazily", async () => {
59
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null);
64
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null);
60
65
 
61
66
  await manager._resolveAddresses();
62
67
 
@@ -66,7 +71,7 @@ describe("DhaliEthChannelManager", () => {
66
71
  });
67
72
 
68
73
  test("resolves destination and contract addresses from provided config", async () => {
69
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
74
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null, publicConfig);
70
75
  await manager._resolveAddresses();
71
76
  expect(manager.destinationAddress).toBe("0x0000000000000000000000000000000000000002");
72
77
  expect(manager.contractAddress).toBe("0x0000000000000000000000000000000000000003");
@@ -74,16 +79,16 @@ describe("DhaliEthChannelManager", () => {
74
79
  });
75
80
 
76
81
  test("calculates channel ID correctly", async () => {
77
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
78
- mockSigner.address = "0x0000000000000000000000000000000000000001";
82
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null, publicConfig);
83
+ const sender = "0x0000000000000000000000000000000000000001";
79
84
  const receiver = "0x0000000000000000000000000000000000000002";
80
85
  const token = "0x0000000000000000000000000000000000000003";
81
86
  const nonce = 12345n;
82
87
 
83
- const expectedId = ethers.keccak256(
84
- ethers.AbiCoder.defaultAbiCoder().encode(
85
- ["address", "address", "address", "uint256"],
86
- ["0x0000000000000000000000000000000000000001", receiver, token, nonce]
88
+ const expectedId = keccak256(
89
+ encodeAbiParameters(
90
+ parseAbiParameters("address, address, address, uint256"),
91
+ [sender, receiver, token, nonce]
87
92
  )
88
93
  );
89
94
 
@@ -98,18 +103,12 @@ describe("DhaliEthChannelManager", () => {
98
103
  .mockResolvedValueOnce(null) // First poll
99
104
  .mockResolvedValueOnce("0x0000000000000000000000000000000000000000000000000000000000000004"); // Second poll
100
105
 
101
- const localManager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
106
+ const localManager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
102
107
  localManager._generateNonce = jest.fn().mockReturnValue(54321n);
103
108
  localManager._calculateChannelId = jest.fn().mockReturnValue("0xCalculatedId");
104
109
 
105
- mockSigner.getAddress.mockResolvedValue("0x0000000000000000000000000000000000000001");
106
- mockSigner.getNonce.mockResolvedValue(10);
107
- mockSigner.estimateGas.mockResolvedValue(BigInt(21000));
108
- mockSigner.sendTransaction.mockResolvedValue({
109
- wait: jest.fn().mockResolvedValue({ status: 1 })
110
- });
111
-
112
- mockProvider.getFeeData = jest.fn().mockResolvedValue({ gasPrice: BigInt(1000000000) });
110
+ mockWalletClient.sendTransaction.mockResolvedValue("0xTxHash");
111
+ mockPublicClient.waitForTransactionReceipt.mockResolvedValue({ status: "success" });
113
112
 
114
113
  configUtils.notifyAdminGateway.mockResolvedValue();
115
114
 
@@ -118,7 +117,7 @@ describe("DhaliEthChannelManager", () => {
118
117
 
119
118
  const receipt = await localManager.deposit(100);
120
119
 
121
- expect(receipt.status).toBe(1);
120
+ expect(receipt.status).toBe("success");
122
121
  expect(configUtils.notifyAdminGateway).toHaveBeenCalledWith(
123
122
  "ETHEREUM",
124
123
  "ETH",
@@ -135,12 +134,14 @@ describe("DhaliEthChannelManager", () => {
135
134
  test("getAuthToken throws if channel not found after polling (REST)", async () => {
136
135
  const mockHttp = jest.fn();
137
136
  configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue(null);
138
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
137
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
139
138
 
140
139
  const originalTimeout = global.setTimeout;
141
140
  global.setTimeout = (cb) => cb();
142
141
 
143
- await expect(manager.getAuthToken(100)).rejects.toThrow(/No open payment channel found in Firestore/);
142
+ const promise = manager.getAuthToken(100);
143
+ await expect(promise).rejects.toThrow(ChannelNotFound);
144
+ await expect(promise).rejects.toThrow(/No open payment channel found in Firestore/);
144
145
 
145
146
  global.setTimeout = originalTimeout;
146
147
  });
@@ -148,18 +149,17 @@ describe("DhaliEthChannelManager", () => {
148
149
  test("getAuthToken defaults to channel capacity if amount is null", async () => {
149
150
  const mockHttp = jest.fn();
150
151
  configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("0x0000000000000000000000000000000000000000000000000000000000000005");
151
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
152
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
152
153
 
153
154
  // Mock on-chain response for getChannel
154
155
  // Selector (4) + 5 words (5 * 32 bytes = 160 bytes = 320 chars)
155
156
  // Amount is word 4 (index 4).
156
157
  // 0x + 64*4 chars of padding + 5000 in hex (padded to 64 chars)
157
158
  const amountHex = BigInt(5000).toString(16).padStart(64, '0');
158
- const mockResult = "0x" + "0".repeat(64 * 4) + amountHex;
159
- mockProvider.call = jest.fn().mockResolvedValue(mockResult);
159
+ const mockResult = { data: "0x" + "0".repeat(64 * 4) + amountHex };
160
+ mockPublicClient.call = jest.fn().mockResolvedValue(mockResult);
160
161
 
161
- mockSigner.getAddress.mockResolvedValue("0x0000000000000000000000000000000000000001");
162
- mockSigner.signTypedData.mockResolvedValue("0xSignature");
162
+ mockWalletClient.signTypedData.mockResolvedValue("0xSignature");
163
163
 
164
164
  const originalTimeout = global.setTimeout;
165
165
  global.setTimeout = (cb) => cb();
@@ -169,11 +169,42 @@ describe("DhaliEthChannelManager", () => {
169
169
 
170
170
  expect(decoded.authorized_to_claim).toBe("5000");
171
171
  expect(decoded.channel_id).toBe("0x0000000000000000000000000000000000000000000000000000000000000005");
172
- expect(mockProvider.call).toHaveBeenCalled();
172
+ expect(mockPublicClient.call).toHaveBeenCalled();
173
173
 
174
174
  global.setTimeout = originalTimeout;
175
175
  });
176
176
 
177
+ test("deposit notifies admin gateway with lowercase address", async () => {
178
+ const mockHttp = jest.fn();
179
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
180
+
181
+ // Setup state for new channel creation
182
+ manager._retrieveChannelIdFromFirestore = jest.fn().mockResolvedValue(null);
183
+ // Provide a valid mixed-case 40-character EIP-55 Ethereum address
184
+ mockWalletClient.getAddresses.mockResolvedValue(["0x71C7656EC7ab88b098defB751B7401B5f6d8976F"]);
185
+ mockWalletClient.sendTransaction = jest.fn().mockResolvedValue("0xhash");
186
+ mockPublicClient.waitForTransactionReceipt = jest.fn().mockResolvedValue({ status: 1 });
187
+
188
+ // Mock polling so deposit finishes
189
+ manager._retrieveChannelIdFromFirestoreWithPolling = jest.fn().mockResolvedValue("0xnewid");
190
+
191
+ // Mock crypto so we can predict channel id or at least verify it's called
192
+ const originalBytes = crypto.randomBytes;
193
+ crypto.randomBytes = jest.fn().mockReturnValue(Buffer.from("00".repeat(32), "hex"));
194
+
195
+ await manager.deposit(100);
196
+
197
+ expect(configUtils.notifyAdminGateway).toHaveBeenCalledWith(
198
+ "ETHEREUM",
199
+ "ETH",
200
+ "0x71c7656ec7ab88b098defb751b7401b5f6d8976f", // Expect perfect lowercase
201
+ expect.any(String),
202
+ mockHttp
203
+ );
204
+
205
+ crypto.randomBytes = originalBytes;
206
+ });
207
+
177
208
  test("getAuthToken polls Firestore (REST)", async () => {
178
209
  const mockHttp = jest.fn();
179
210
  configUtils.retrieveChannelIdFromFirestoreRest
@@ -181,12 +212,11 @@ describe("DhaliEthChannelManager", () => {
181
212
  .mockResolvedValueOnce("0x0000000000000000000000000000000000000000000000000000000000000005"); // Second poll
182
213
 
183
214
  const amountHex = BigInt(1000).toString(16).padStart(64, '0');
184
- const mockResult = "0x" + "0".repeat(64 * 4) + amountHex;
185
- mockProvider.call = jest.fn().mockResolvedValue(mockResult);
215
+ const mockResult = { data: "0x" + "0".repeat(64 * 4) + amountHex };
216
+ mockPublicClient.call = jest.fn().mockResolvedValue(mockResult);
186
217
 
187
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
188
- mockSigner.getAddress.mockResolvedValue("0x0000000000000000000000000000000000000001");
189
- mockSigner.signTypedData.mockResolvedValue("0xSignature");
218
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
219
+ mockWalletClient.signTypedData.mockResolvedValue("0xSignature");
190
220
 
191
221
  const originalTimeout = global.setTimeout;
192
222
  global.setTimeout = (cb) => cb();
@@ -201,8 +231,8 @@ describe("DhaliEthChannelManager", () => {
201
231
 
202
232
  test("queries Firestore with lowercase address (REST)", async () => {
203
233
  const mockHttp = jest.fn();
204
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
205
- mockSigner.getAddress.mockResolvedValue("0xMixEdCaSeAdDrEsS");
234
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
235
+ mockWalletClient.getAddresses.mockResolvedValue(["0xMixEdCaSeAdDrEsS"]);
206
236
 
207
237
  await manager._retrieveChannelIdFromFirestore();
208
238
 
@@ -215,8 +245,8 @@ describe("DhaliEthChannelManager", () => {
215
245
  });
216
246
 
217
247
  test("uses default REST if no function provided", async () => {
218
- const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
219
- mockSigner.getAddress.mockResolvedValue("0xMyAddr");
248
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, null, publicConfig);
249
+ mockWalletClient.getAddresses.mockResolvedValue(["0xMyAddr"]);
220
250
  configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("0xRestId");
221
251
 
222
252
  const id = await manager._retrieveChannelIdFromFirestore();
@@ -228,4 +258,40 @@ describe("DhaliEthChannelManager", () => {
228
258
  fetch
229
259
  );
230
260
  });
261
+
262
+ test("performs lower-casing on EVM addresses consistently", async () => {
263
+ const mixedAddr = "0x71C7656EC7ab88b098defB751B7401B5f6d8976F";
264
+ const lowerAddr = mixedAddr.toLowerCase();
265
+
266
+ const mockHttp = jest.fn();
267
+ const manager = new DhaliEthChannelManager(mockWalletClient, mockPublicClient, currency, mockHttp, publicConfig);
268
+ mockWalletClient.getAddresses.mockResolvedValue([mixedAddr]);
269
+
270
+ // 1. Check firestore retrieval
271
+ configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("0xid");
272
+ await manager._retrieveChannelIdFromFirestore();
273
+ expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
274
+ expect.anything(),
275
+ expect.anything(),
276
+ lowerAddr,
277
+ mockHttp
278
+ );
279
+
280
+ // 2. Check gateway notification during deposit (Open Channel path)
281
+ configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue(null);
282
+ manager._generateNonce = jest.fn().mockReturnValue(1n);
283
+ manager._calculateChannelId = jest.fn().mockReturnValue("0xid");
284
+ mockWalletClient.sendTransaction.mockResolvedValue("0xhash");
285
+ mockPublicClient.waitForTransactionReceipt.mockResolvedValue({ status: "success" });
286
+ manager._retrieveChannelIdFromFirestoreWithPolling = jest.fn().mockResolvedValue("0xid");
287
+
288
+ await manager.deposit(100);
289
+ expect(configUtils.notifyAdminGateway).toHaveBeenCalledWith(
290
+ expect.anything(),
291
+ expect.anything(),
292
+ lowerAddr,
293
+ expect.anything(),
294
+ mockHttp
295
+ );
296
+ });
231
297
  });