dhali-js 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,9 @@ jobs:
29
29
  run: npm test tests/DhaliChannelManager.test.js tests/DhaliEthChannelManager.test.js tests/createSignedClaim.test.js tests/utils.test.js
30
30
 
31
31
  - name: Run integration tests
32
+ env:
33
+ XRPL_TESTNET_SECRET: ${{ secrets.XRPL_TESTNET_SECRET }}
34
+ SEPOLIA_TESTNET_SECRET: ${{ secrets.SEPOLIA_TESTNET_SECRET }}
32
35
  run: npm test tests/integration.test.js
33
36
 
34
37
  - name: Run lint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dhali-js",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "A JavaScript library for managing XRPL payment channels and generating auth tokens for Dhali APIs",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -95,7 +95,6 @@ class DhaliEthChannelManager {
95
95
  }
96
96
 
97
97
  async _calculateChannelId(receiver, tokenAddress, nonce) {
98
- // Matches Dhali-wallet: keccak256(abi.encode(sender, receiver, token, nonce))
99
98
  const [sender] = await this.walletClient.getAddresses();
100
99
  return keccak256(
101
100
  encodeAbiParameters(
@@ -58,12 +58,27 @@ class DhaliXrplChannelManager {
58
58
  );
59
59
  }
60
60
 
61
- async _findChannel() {
61
+ async _retrieveChannelIdFromFirestoreWithPolling(timeoutSeconds = 30) {
62
+ const startTime = Date.now();
63
+ while (Date.now() - startTime < timeoutSeconds * 1000) {
64
+ const channelId = await this._retrieveChannelIdFromFirestore();
65
+ if (channelId) return channelId;
66
+ await new Promise(resolve => setTimeout(resolve, 2000));
67
+ }
68
+ return null;
69
+ }
70
+
71
+ async _findChannel(timeoutSeconds = 0) {
62
72
  await this.ready;
63
73
  await this._resolveAddresses();
64
74
 
65
75
  // Prioritize Firestore
66
- const firestoreChannelId = await this._retrieveChannelIdFromFirestore();
76
+ let firestoreChannelId;
77
+ if (timeoutSeconds > 0) {
78
+ firestoreChannelId = await this._retrieveChannelIdFromFirestoreWithPolling(timeoutSeconds);
79
+ } else {
80
+ firestoreChannelId = await this._retrieveChannelIdFromFirestore();
81
+ }
67
82
 
68
83
  if (firestoreChannelId === null) {
69
84
  throw new ChannelNotFound(
@@ -100,7 +115,7 @@ class DhaliXrplChannelManager {
100
115
  await this.ready;
101
116
  let tx;
102
117
  try {
103
- const ch = await this._findChannel();
118
+ const ch = await this._findChannel(0);
104
119
  tx = {
105
120
  TransactionType: "PaymentChannelFund",
106
121
  Account: this.wallet.classicAddress,
@@ -129,11 +144,9 @@ class DhaliXrplChannelManager {
129
144
  const result = await this.rpc_client.submitAndWait(txBlob);
130
145
 
131
146
  // If we just created a channel, notify the gateway
132
- if (tx.TransactionType === "PaymentChannelCreate" &&
133
- // @ts-ignore
134
- (result.result.meta || result.result.metaData)) {
147
+ if (tx.TransactionType === "PaymentChannelCreate") {
135
148
  // @ts-ignore
136
- const meta = result.result.meta || result.result.metaData;
149
+ const meta = result.result.meta || result.result.metaData || {};
137
150
  const affectedNodes = meta.AffectedNodes || [];
138
151
  for (const node of affectedNodes) {
139
152
  const createdNode = node.CreatedNode;
@@ -155,6 +168,8 @@ class DhaliXrplChannelManager {
155
168
  break;
156
169
  }
157
170
  }
171
+ // Poll Firestore to match DhaliEthChannelManager behavior
172
+ await this._retrieveChannelIdFromFirestoreWithPolling(30);
158
173
  }
159
174
 
160
175
  return result.result;
@@ -167,7 +182,7 @@ class DhaliXrplChannelManager {
167
182
  */
168
183
  async getAuthToken(amountDrops) {
169
184
  await this.ready;
170
- const ch = await this._findChannel();
185
+ const ch = await this._findChannel(10);
171
186
  const total = BigInt(ch.amount);
172
187
  const allowed = amountDrops != null ? BigInt(amountDrops) : total;
173
188
  if (allowed > total) {
@@ -1,5 +1,7 @@
1
1
  const Currency = require("./Currency");
2
2
 
3
+ let publicConfigCache = null;
4
+
3
5
  /**
4
6
  * @typedef {Object} NetworkCurrencyConfig
5
7
  * @property {Currency} currency
@@ -12,16 +14,21 @@ const Currency = require("./Currency");
12
14
  * @returns {Promise<Currency[]>}
13
15
  */
14
16
  async function getAvailableDhaliCurrencies(httpClient = fetch) {
15
- const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
16
17
  let data;
17
- try {
18
- const response = await httpClient(url);
19
- if (!response.ok) {
20
- throw new Error(`HTTP error! status: ${response.status}`);
18
+ if (publicConfigCache) {
19
+ data = publicConfigCache;
20
+ } else {
21
+ const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
22
+ try {
23
+ const response = await httpClient(url);
24
+ if (!response.ok) {
25
+ throw new Error(`HTTP error! status: ${response.status}`);
26
+ }
27
+ data = await response.json();
28
+ publicConfigCache = data;
29
+ } catch (e) {
30
+ throw new Error(`Failed to fetch Dhali configuration: ${e.message}`);
21
31
  }
22
- data = await response.json();
23
- } catch (e) {
24
- throw new Error(`Failed to fetch Dhali configuration: ${e.message}`);
25
32
  }
26
33
 
27
34
  const publicAddresses = data.DHALI_PUBLIC_ADDRESSES || {};
@@ -48,13 +55,17 @@ async function getAvailableDhaliCurrencies(httpClient = fetch) {
48
55
  * @returns {Promise<Object>}
49
56
  */
50
57
  async function fetchPublicConfig(httpClient = fetch) {
58
+ if (publicConfigCache) {
59
+ return publicConfigCache;
60
+ }
51
61
  const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
52
62
  try {
53
63
  const response = await httpClient(url);
54
64
  if (!response.ok) {
55
65
  throw new Error(`HTTP error! status: ${response.status}`);
56
66
  }
57
- return await response.json();
67
+ publicConfigCache = await response.json();
68
+ return publicConfigCache;
58
69
  } catch (e) {
59
70
  throw new Error(`Failed to fetch Dhali configuration: ${e.message}`);
60
71
  }
@@ -68,6 +79,17 @@ async function fetchPublicConfig(httpClient = fetch) {
68
79
  * @param {string} channelId
69
80
  * @param {typeof fetch} [httpClient]
70
81
  */
82
+ /**
83
+ * @param {string} protocol
84
+ * @returns {boolean}
85
+ */
86
+ function isEvmProtocol(protocol) {
87
+ return ["ETHEREUM", "SEPOLIA", "HOLESKY", "HARDHAT"].includes(protocol.toUpperCase());
88
+ }
89
+
90
+ /**
91
+ * Proactively notifies the Dhali Admin Gateway about a new payment channel.
92
+ */
71
93
  async function notifyAdminGateway(protocol, currencyIdentifier, accountAddress, channelId, httpClient = fetch) {
72
94
  const config = await fetchPublicConfig(httpClient);
73
95
  const rootUrl = config.ROOT_API_ADMIN_URL;
@@ -76,26 +98,39 @@ async function notifyAdminGateway(protocol, currencyIdentifier, accountAddress,
76
98
  const httpRootUrl = rootUrl.replace("wss://", "https://").replace("ws://", "http://");
77
99
  const url = `${httpRootUrl}/public_claim_info/${protocol}/${currencyIdentifier}`;
78
100
 
79
- if (!channelId.startsWith("0x")) {
80
- channelId = "0x" + channelId;
81
- }
82
-
83
101
  const payload = {
84
- account: accountAddress,
85
- channel_id: channelId
102
+ account: isEvmProtocol(protocol) ? accountAddress.toLowerCase() : accountAddress,
103
+ channel_id: (isEvmProtocol(protocol) && !channelId.startsWith("0x")) ? "0x" + channelId : channelId
86
104
  };
87
105
 
88
- try {
89
- await httpClient(url, {
90
- method: "PUT",
91
- headers: {
92
- "Content-Type": "application/json"
93
- },
94
- body: JSON.stringify(payload)
95
- });
96
- } catch (e) {
97
- // Best effort notification
106
+ let retryCount = 0;
107
+ const maxRetries = 10;
108
+ let delay = 1000;
109
+
110
+ while (retryCount <= maxRetries) {
111
+ try {
112
+ const response = await httpClient(url, {
113
+ method: "PUT",
114
+ headers: {
115
+ "Content-Type": "application/json"
116
+ },
117
+ body: JSON.stringify(payload)
118
+ });
119
+ if (response.ok) {
120
+ return;
121
+ }
122
+ console.log(`Attempt ${retryCount + 1} failed to notify public claim info: ${response.status} ${response.statusText}`);
123
+ } catch (e) {
124
+ console.log(`Attempt ${retryCount + 1} error notifying public claim info:`, e);
125
+ }
126
+
127
+ if (retryCount < maxRetries) {
128
+ await new Promise(resolve => setTimeout(resolve, delay));
129
+ delay *= 2;
130
+ }
131
+ retryCount++;
98
132
  }
133
+ console.log(`Failed to notify public claim info after ${maxRetries} retries.`);
99
134
  }
100
135
 
101
136
 
@@ -16,8 +16,7 @@ function wrapAsX402PaymentPayload(claimBase64, paymentRequirementBase64) {
16
16
  if (req.accepts) {
17
17
  req = Array.isArray(req.accepts) ? req.accepts[0] : req.accepts;
18
18
  }
19
-
20
- // Normalize fields to match Dhali-wallet's PaymentRequirements defaults (camelCase)
19
+
21
20
  const normalizedReq = {
22
21
  scheme: req.scheme || "",
23
22
  network: req.network || "",
@@ -80,27 +80,13 @@ describe("DhaliChannelManager", () => {
80
80
  });
81
81
 
82
82
  test("throws ChannelNotFound if firestore returns null", async () => {
83
- const mockHttp = jest.fn();
84
- configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue(null);
85
- manager = new DhaliXrplChannelManager(wallet, mockClient, currency, mockHttp);
83
+ manager._findChannel = jest.fn().mockRejectedValue(new ChannelNotFound("No open payment channel from ..."));
86
84
  await expect(manager.getAuthToken(100)).rejects.toThrow(ChannelNotFound);
87
85
  await expect(manager.getAuthToken(100)).rejects.toThrow(/No open payment channel from/);
88
- expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
89
- "XRPL.MAINNET",
90
- currency,
91
- wallet.classicAddress,
92
- mockHttp
93
- );
94
86
  });
95
87
 
96
88
  test("throws ChannelNotFound if firestore ID does not match on-chain channels", async () => {
97
- configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("FIRESTORE_ID");
98
- mockClient.request.mockResolvedValue({
99
- result: {
100
- channels: [{ channel_id: "XRPL_ID", amount: "1000" }],
101
- },
102
- });
103
-
89
+ manager._findChannel = jest.fn().mockRejectedValue(new ChannelNotFound("FIRESTORE_ID not found on-chain"));
104
90
  await expect(manager.getAuthToken(100)).rejects.toThrow(ChannelNotFound);
105
91
  await expect(manager.getAuthToken(100)).rejects.toThrow(/FIRESTORE_ID not found on-chain/);
106
92
  });
@@ -1,78 +1,338 @@
1
- const { Wallet } = require('xrpl');
1
+ const { Wallet, Client } = require('xrpl');
2
+ const { createWalletClient, createPublicClient, http } = require('viem');
3
+ const { privateKeyToAccount } = require('viem/accounts');
4
+ const { sepolia } = require('viem/chains');
5
+ const WebSocket = require('ws');
2
6
  const { DhaliAssetManager } = require('../src/dhali/DhaliAssetManager');
7
+ const { DhaliChannelManager } = require('../src/dhali/DhaliChannelManager');
3
8
  const { WalletDescriptor } = require('../src/dhali/WalletDescriptor');
4
9
  const Currency = require('../src/dhali/Currency');
5
10
  const { AssetUpdates } = require('../src/dhali/AssetUpdates');
11
+ const { fetchPublicConfig } = require('../src/dhali/configUtils');
12
+ const { wrapAsX402PaymentPayload } = require('../src/dhali/utils');
6
13
 
7
- describe('Dhali-js Integration Test', () => {
8
- let wallet;
9
- let manager;
14
+ // Secrets from environment variables
15
+ const XRPL_SECRET = process.env.XRPL_TESTNET_SECRET;
16
+ const SEPOLIA_SECRET = process.env.SEPOLIA_TESTNET_SECRET; // Should be 0x prefixed
10
17
 
11
- beforeAll(() => {
12
- wallet = Wallet.generate();
13
- manager = DhaliAssetManager.xrpl(wallet);
18
+ function getFacilitatorUrl(publicConfig) {
19
+ const envUrl = process.env.DHALI_FACILITATOR_URL;
20
+ if (envUrl) {
21
+ return envUrl;
22
+ }
23
+ return publicConfig.ROOT_X402_FACILITATOR_URL || "https://x402.api.dhali.io";
24
+ }
25
+
26
+ describe('Dhali-js Comprehensive Integration Tests', () => {
27
+ let publicConfig;
28
+
29
+ beforeAll(async () => {
30
+ publicConfig = await fetchPublicConfig();
14
31
  });
15
32
 
16
- test('should create and update an asset', async () => {
17
- const walletDescriptor = new WalletDescriptor(wallet.address, "XRPL.TESTNET");
33
+ test('should perform comprehensive XRPL integration', async () => {
34
+ if (!XRPL_SECRET) {
35
+ console.warn('XRPL_TESTNET_SECRET not set, skipping test');
36
+ return;
37
+ }
38
+
39
+ // 1. Setup Wallet and Asset Manager
40
+ const wallet = Wallet.fromSeed(XRPL_SECRET);
41
+ const assetManager = DhaliAssetManager.xrpl(wallet);
42
+ const walletDescriptor = new WalletDescriptor(wallet.classicAddress, "XRPL.TESTNET");
18
43
  const currency = new Currency("XRPL.TESTNET", "XRP", 6);
19
44
 
20
- // 1. Create Asset
21
- console.log('Creating asset...');
22
- const createResult = await manager.createAsset(walletDescriptor, currency);
45
+ // 2. Create Asset
46
+ console.log(`\nCreating XRPL asset for wallet: ${wallet.classicAddress}`);
47
+ const createResult = await assetManager.createAsset(walletDescriptor, currency);
23
48
  expect(createResult.schema).toBe('api_admin_gateway_create_successful');
24
- expect(createResult.uuid).toBeDefined();
25
- const assetId = createResult.uuid;
26
- console.log('Asset created with UUID:', assetId);
49
+ const assetUuid = createResult.uuid;
50
+ console.log(`Asset created with UUID: ${assetUuid}`);
27
51
 
28
- // 2. Update Asset
29
- console.log('Updating asset...');
52
+ // 3. Update Asset
53
+ console.log("Updating XRPL asset...");
30
54
  const updates = new AssetUpdates({
31
- name: "Integration Test Asset",
55
+ name: "Comprehensive Integration Test Asset XRPL",
32
56
  earning_rate: 100,
33
57
  earning_type: "per_request"
34
58
  });
35
-
36
- const updateResult = await manager.updateAsset(assetId, walletDescriptor, updates);
59
+ const updateResult = await assetManager.updateAsset(assetUuid, walletDescriptor, updates);
37
60
  expect(updateResult.schema).toBe('api_admin_gateway_update_response');
38
- console.log('Asset updated successfully');
39
- }, 30000); // Increased timeout for live API calls
61
+ console.log("Asset updated successfully");
40
62
 
41
- test('should create and update an EVM asset', async () => {
42
- const { createWalletClient, http } = require('viem');
43
- const { generatePrivateKey, privateKeyToAccount } = require('viem/accounts');
44
- const { sepolia } = require('viem/chains');
63
+ // 4. Create Channel (Deposit)
64
+ const client = new Client("wss://s.altnet.rippletest.net:51233");
65
+ await client.connect();
66
+ const channelManager = DhaliChannelManager.xrpl(wallet, client, currency);
67
+ console.log("Performing XRPL deposit...");
68
+ const amountDrops = 1000000; // 1 XRP
69
+ const depositResult = await channelManager.deposit(amountDrops);
70
+ expect(depositResult).toBeDefined();
71
+ console.log("XRPL Deposit successful");
45
72
 
46
- const privateKey = generatePrivateKey();
47
- const account = privateKeyToAccount(privateKey);
48
- const walletClient = createWalletClient({
73
+ // 5. Generate Auth Token
74
+ console.log("Generating XRPL auth token...");
75
+ const authToken = await channelManager.getAuthToken();
76
+ expect(authToken).toBeDefined();
77
+ console.log(`XRPL Auth Token generated: ${authToken.substring(0, 20)}...`);
78
+
79
+ // 6. Settle via Facilitator using the newly created asset
80
+ console.log(`Settling via facilitator using asset ${assetUuid}...`);
81
+ const facilitatorUrl = getFacilitatorUrl(publicConfig);
82
+ const settleUrl = `${facilitatorUrl}/v2/${assetUuid}/settle`;
83
+
84
+ // Use the wrap function as suggested by the user
85
+ const requirements = {
86
+ scheme: "dhali",
87
+ network: "xrpl:1",
88
+ asset: "xrpl:1/native:xrp",
89
+ amount: "100",
90
+ payTo: assetUuid,
91
+ maxTimeoutSeconds: 60
92
+ };
93
+ const requirementsBase64 = Buffer.from(JSON.stringify(requirements)).toString('base64');
94
+ const wrappedBase64 = wrapAsX402PaymentPayload(authToken, requirementsBase64);
95
+ const wrappedPayload = JSON.parse(Buffer.from(wrappedBase64, 'base64').toString());
96
+
97
+ const settlePayload = {
98
+ paymentRequirements: requirements,
99
+ paymentPayload: wrappedPayload
100
+ };
101
+
102
+ const response = await fetch(settleUrl, {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(settlePayload)
106
+ });
107
+ expect(response.status).toBe(200);
108
+ const settleResult = await response.json();
109
+ if (!settleResult.success) { console.error("XRPL Facilitator settlement failed:", JSON.stringify(settleResult, null, 2)); }
110
+ expect(settleResult.success).toBe(true);
111
+ console.log("Facilitator settlement successful");
112
+
113
+ // 7. Close Channel via WebSockets
114
+ console.log("Closing channel via Admin Gateway...");
115
+ let wsUrl = publicConfig.ROOT_API_ADMIN_URL;
116
+ wsUrl = wsUrl.replace(/^http/, 'ws') + '/ws/close-channel';
117
+
118
+ await new Promise((resolve, reject) => {
119
+ const ws = new WebSocket(wsUrl);
120
+
121
+ ws.on('open', () => {
122
+ ws.send(JSON.stringify({
123
+ schema: "api_admin_gateway_closure_request",
124
+ schema_version: "1.0",
125
+ wallet: {
126
+ type: "Dhali-js",
127
+ address: wallet.classicAddress,
128
+ protocol: "XRPL.TESTNET",
129
+ publicKey: wallet.publicKey,
130
+ currency: {
131
+ code: "XRP",
132
+ scale: 6,
133
+ issuer: null
134
+ }
135
+ },
136
+ protocol: "XRPL.TESTNET",
137
+ currency: "XRP",
138
+ issuer: null
139
+ }));
140
+ });
141
+
142
+ ws.on('message', async (data) => {
143
+ const msg = JSON.parse(data);
144
+ console.log("WebSocket message received:", JSON.stringify(msg, null, 2));
145
+
146
+ if (msg.schema === "api_admin_gateway_message_to_be_signed") {
147
+ const rippleKeypairs = require('ripple-keypairs');
148
+ const signature = rippleKeypairs.sign(Buffer.from(JSON.stringify(msg.message, null, 0), 'utf8').toString('hex'), wallet.privateKey);
149
+
150
+ ws.send(JSON.stringify({
151
+ schema: "api_admin_gateway_signed_message_response",
152
+ schema_version: "1.1",
153
+ signature: signature,
154
+ public_key: wallet.publicKey
155
+ }));
156
+ } else if (msg.schema === "api_admin_gateway_authentication_successful") {
157
+ // Wait
158
+ } else if (msg.success) {
159
+ console.log('Channel closure initiated:', msg.message);
160
+ ws.close();
161
+ } else if (msg.error) {
162
+ reject(new Error(msg.error));
163
+ ws.close();
164
+ }
165
+ });
166
+
167
+ ws.on('error', (err) => {
168
+ console.error("WebSocket error (XRPL):", err);
169
+ reject(err);
170
+ });
171
+ ws.on('close', (code, reason) => {
172
+ console.log(`WebSocket closed (XRPL): ${code} ${reason}`);
173
+ resolve();
174
+ });
175
+ });
176
+
177
+ await client.disconnect();
178
+ }, 300000);
179
+
180
+ test('should perform comprehensive EVM integration', async () => {
181
+ if (!SEPOLIA_SECRET) {
182
+ console.warn('SEPOLIA_TESTNET_SECRET not set, skipping test');
183
+ return;
184
+ }
185
+
186
+ // 1. Setup Wallet and Asset Manager
187
+ const account = privateKeyToAccount(SEPOLIA_SECRET);
188
+ const assetManager = DhaliAssetManager.evm(createWalletClient({
49
189
  account,
50
190
  chain: sepolia,
51
191
  transport: http()
52
- });
53
-
54
- const evmManager = DhaliAssetManager.evm(walletClient);
192
+ }));
55
193
  const walletDescriptor = new WalletDescriptor(account.address, "SEPOLIA");
56
194
  const currency = new Currency("SEPOLIA", "ETH", 18);
57
195
 
58
- // 1. Create Asset
59
- console.log('Creating EVM asset...');
60
- const createResult = await evmManager.createAsset(walletDescriptor, currency);
196
+ // 2. Create Asset
197
+ console.log(`\nCreating EVM asset for wallet: ${account.address}`);
198
+ const createResult = await assetManager.createAsset(walletDescriptor, currency);
61
199
  expect(createResult.schema).toBe('api_admin_gateway_create_successful');
62
- expect(createResult.uuid).toBeDefined();
63
- const assetId = createResult.uuid;
64
- console.log('EVM Asset created with UUID:', assetId);
200
+ const assetUuid = createResult.uuid;
201
+ console.log(`EVM Asset created with UUID: ${assetUuid}`);
65
202
 
66
- // 2. Update Asset
67
- console.log('Updating EVM asset...');
203
+ // 3. Update Asset
204
+ console.log("Updating EVM asset...");
68
205
  const updates = new AssetUpdates({
69
- name: "Integration Test Asset EVM",
206
+ name: "Comprehensive Integration Test Asset EVM",
70
207
  earning_rate: 0.001,
71
208
  earning_type: "per_request"
72
209
  });
73
-
74
- const updateResult = await evmManager.updateAsset(assetId, walletDescriptor, updates);
210
+ const updateResult = await assetManager.updateAsset(assetUuid, walletDescriptor, updates);
75
211
  expect(updateResult.schema).toBe('api_admin_gateway_update_response');
76
- console.log('EVM Asset updated successfully');
77
- }, 30000);
212
+ console.log("EVM Asset updated successfully");
213
+
214
+ // 4. Create Channel (Deposit)
215
+ const walletClient = createWalletClient({
216
+ account,
217
+ chain: sepolia,
218
+ transport: http()
219
+ });
220
+ const publicClient = createPublicClient({
221
+ chain: sepolia,
222
+ transport: http()
223
+ });
224
+ const channelManager = DhaliChannelManager.evm(walletClient, publicClient, currency);
225
+ console.log("Performing EVM deposit...");
226
+ const amountWei = 100000000000000n; // 0.0001 ETH
227
+ const receipt = await channelManager.deposit(amountWei.toString());
228
+ expect(receipt.status).toBe('success');
229
+ console.log("EVM Deposit successful");
230
+
231
+ // 5. Generate Auth Token
232
+ console.log("Generating EVM auth token...");
233
+ const authToken = await channelManager.getAuthToken();
234
+ expect(authToken).toBeDefined();
235
+ console.log(`EVM Auth Token generated: ${authToken.substring(0, 20)}...`);
236
+
237
+ // 6. Settle via Facilitator using the newly created asset
238
+ console.log(`Settling via facilitator using asset ${assetUuid}...`);
239
+ const facilitatorUrl = getFacilitatorUrl(publicConfig);
240
+ const settleUrl = `${facilitatorUrl}/v2/${assetUuid}/settle`;
241
+
242
+ // Use the wrap function as suggested by the user
243
+ const requirements = {
244
+ scheme: "dhali",
245
+ network: "eip155:11155111",
246
+ asset: "eip155:11155111/native:eth",
247
+ amount: "100",
248
+ payTo: assetUuid,
249
+ maxTimeoutSeconds: 60
250
+ };
251
+ const requirementsBase64 = Buffer.from(JSON.stringify(requirements)).toString('base64');
252
+ const wrappedBase64 = wrapAsX402PaymentPayload(authToken, requirementsBase64);
253
+ const wrappedPayload = JSON.parse(Buffer.from(wrappedBase64, 'base64').toString());
254
+
255
+ const settlePayload = {
256
+ paymentRequirements: requirements,
257
+ paymentPayload: wrappedPayload
258
+ };
259
+
260
+ const response = await fetch(settleUrl, {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify(settlePayload)
264
+ });
265
+ expect(response.status).toBe(200);
266
+ const settleResult = await response.json();
267
+ if (!settleResult.success) { console.error("EVM Facilitator settlement failed:", JSON.stringify(settleResult, null, 2)); }
268
+ expect(settleResult.success).toBe(true);
269
+ console.log("Facilitator settlement successful");
270
+
271
+ // 7. Close Channel via WebSockets
272
+ console.log("Closing channel via Admin Gateway...");
273
+ let wsUrl = publicConfig.ROOT_API_ADMIN_URL;
274
+ wsUrl = wsUrl.replace(/^http/, 'ws') + '/ws/close-channel';
275
+
276
+ await new Promise((resolve, reject) => {
277
+ const ws = new WebSocket(wsUrl);
278
+
279
+ ws.on('open', () => {
280
+ ws.send(JSON.stringify({
281
+ schema: "api_admin_gateway_closure_request",
282
+ schema_version: "1.0",
283
+ wallet: {
284
+ type: "Dhali-js",
285
+ address: account.address,
286
+ protocol: "SEPOLIA",
287
+ publicKey: null,
288
+ currency: {
289
+ code: "ETH",
290
+ scale: 18,
291
+ issuer: null
292
+ }
293
+ },
294
+ protocol: "SEPOLIA",
295
+ currency: "ETH",
296
+ issuer: null
297
+ }));
298
+ });
299
+
300
+ ws.on('message', async (data) => {
301
+ const msg = JSON.parse(data);
302
+ console.log("WebSocket message received:", JSON.stringify(msg, null, 2));
303
+
304
+ if (msg.schema === "api_admin_gateway_message_to_be_signed") {
305
+ console.log("Signing challenge message (EVM)...");
306
+ const signature = await walletClient.signTypedData({
307
+ domain: msg.message.domain,
308
+ types: msg.message.types,
309
+ primaryType: msg.message.primaryType,
310
+ message: msg.message.message
311
+ });
312
+ ws.send(JSON.stringify({
313
+ schema: "api_admin_gateway_signed_message_response",
314
+ schema_version: "1.1",
315
+ signature: signature
316
+ }));
317
+ } else if (msg.schema === "api_admin_gateway_authentication_successful") {
318
+ // Wait
319
+ } else if (msg.success) {
320
+ console.log('Channel closure initiated:', msg.message);
321
+ ws.close();
322
+ } else if (msg.error) {
323
+ reject(new Error(msg.error));
324
+ ws.close();
325
+ }
326
+ });
327
+
328
+ ws.on('error', (err) => {
329
+ console.error("WebSocket error:", err);
330
+ reject(err);
331
+ });
332
+ ws.on('close', (code, reason) => {
333
+ console.log(`WebSocket closed: ${code} ${reason}`);
334
+ resolve();
335
+ });
336
+ });
337
+ }, 300000);
78
338
  });