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.
- package/.github/workflows/tests.yaml +3 -0
- package/package.json +1 -1
- package/src/dhali/DhaliEthChannelManager.js +0 -1
- package/src/dhali/DhaliXrplChannelManager.js +23 -8
- package/src/dhali/configUtils.js +60 -25
- package/src/dhali/utils.js +1 -2
- package/tests/DhaliChannelManager.test.js +2 -16
- package/tests/integration.test.js +305 -45
|
@@ -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
|
@@ -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
|
|
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
|
-
|
|
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) {
|
package/src/dhali/configUtils.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
package/src/dhali/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
17
|
-
|
|
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
|
-
//
|
|
21
|
-
console.log(
|
|
22
|
-
const createResult = await
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
console.log('Asset created with UUID:', assetId);
|
|
49
|
+
const assetUuid = createResult.uuid;
|
|
50
|
+
console.log(`Asset created with UUID: ${assetUuid}`);
|
|
27
51
|
|
|
28
|
-
//
|
|
29
|
-
console.log(
|
|
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(
|
|
39
|
-
}, 30000); // Increased timeout for live API calls
|
|
61
|
+
console.log("Asset updated successfully");
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
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
|
-
//
|
|
59
|
-
console.log(
|
|
60
|
-
const createResult = await
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
67
|
-
console.log(
|
|
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(
|
|
77
|
-
|
|
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
|
});
|