dhali-js 1.0.4 → 2.1.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/.github/workflows/tests.yaml +3 -0
- package/README.md +83 -75
- package/package.json +5 -3
- package/src/dhali/Currency.js +9 -0
- package/src/dhali/DhaliChannelManager.js +30 -101
- package/src/dhali/DhaliEthChannelManager.js +331 -0
- package/src/dhali/DhaliXrplChannelManager.js +199 -0
- package/src/dhali/configUtils.js +203 -0
- package/src/dhali/createSignedClaim.js +34 -0
- package/src/dhali/utils.js +44 -0
- package/src/index.js +11 -10
- package/tests/DhaliChannelManager.test.js +76 -146
- package/tests/DhaliEthChannelManager.test.js +231 -0
- package/tests/utils.test.js +70 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const { DhaliEthChannelManager } = require("../src/dhali/DhaliEthChannelManager");
|
|
2
|
+
const Currency = require("../src/dhali/Currency");
|
|
3
|
+
const { ethers } = require("ethers");
|
|
4
|
+
const configUtils = require("../src/dhali/configUtils");
|
|
5
|
+
|
|
6
|
+
jest.mock("../src/dhali/configUtils");
|
|
7
|
+
|
|
8
|
+
describe("DhaliEthChannelManager", () => {
|
|
9
|
+
let mockSigner;
|
|
10
|
+
let mockProvider;
|
|
11
|
+
let currency;
|
|
12
|
+
let publicConfig;
|
|
13
|
+
let manager;
|
|
14
|
+
|
|
15
|
+
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()
|
|
22
|
+
};
|
|
23
|
+
mockProvider = {};
|
|
24
|
+
currency = new Currency("ETH", 18);
|
|
25
|
+
publicConfig = {
|
|
26
|
+
DHALI_PUBLIC_ADDRESSES: {
|
|
27
|
+
ETHEREUM: {
|
|
28
|
+
ETH: { wallet_id: "0x0000000000000000000000000000000000000002" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
CONTRACTS: {
|
|
32
|
+
ETHEREUM: { contract_address: "0x0000000000000000000000000000000000000003" }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
configUtils.fetchPublicConfig.mockResolvedValue(publicConfig);
|
|
36
|
+
manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("initializes without default http client", () => {
|
|
41
|
+
const localManager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
|
|
42
|
+
expect(localManager.httpClient).toBe(fetch);
|
|
43
|
+
expect(localManager.chainId).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("initializes with provided http client", () => {
|
|
47
|
+
const mockHttp = jest.fn();
|
|
48
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
49
|
+
expect(manager.httpClient).toBe(mockHttp);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("initializes without config or addresses (lazy resolution)", () => {
|
|
53
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null);
|
|
54
|
+
expect(manager.destinationAddress).toBeUndefined();
|
|
55
|
+
expect(manager.contractAddress).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("resolves addresses lazily", async () => {
|
|
59
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null);
|
|
60
|
+
|
|
61
|
+
await manager._resolveAddresses();
|
|
62
|
+
|
|
63
|
+
expect(manager.destinationAddress).toBe("0x0000000000000000000000000000000000000002");
|
|
64
|
+
expect(manager.contractAddress).toBe("0x0000000000000000000000000000000000000003");
|
|
65
|
+
expect(configUtils.fetchPublicConfig).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("resolves destination and contract addresses from provided config", async () => {
|
|
69
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
|
|
70
|
+
await manager._resolveAddresses();
|
|
71
|
+
expect(manager.destinationAddress).toBe("0x0000000000000000000000000000000000000002");
|
|
72
|
+
expect(manager.contractAddress).toBe("0x0000000000000000000000000000000000000003");
|
|
73
|
+
expect(configUtils.fetchPublicConfig).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("calculates channel ID correctly", async () => {
|
|
77
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, null, publicConfig);
|
|
78
|
+
mockSigner.address = "0x0000000000000000000000000000000000000001";
|
|
79
|
+
const receiver = "0x0000000000000000000000000000000000000002";
|
|
80
|
+
const token = "0x0000000000000000000000000000000000000003";
|
|
81
|
+
const nonce = 12345n;
|
|
82
|
+
|
|
83
|
+
const expectedId = ethers.keccak256(
|
|
84
|
+
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
85
|
+
["address", "address", "address", "uint256"],
|
|
86
|
+
["0x0000000000000000000000000000000000000001", receiver, token, nonce]
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const id = await manager._calculateChannelId(receiver, token, nonce);
|
|
91
|
+
expect(id).toBe(expectedId);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("deposit with polling when creating a new channel", async () => {
|
|
95
|
+
const mockHttp = jest.fn();
|
|
96
|
+
configUtils.retrieveChannelIdFromFirestoreRest
|
|
97
|
+
.mockResolvedValueOnce(null) // First check in deposit()
|
|
98
|
+
.mockResolvedValueOnce(null) // First poll
|
|
99
|
+
.mockResolvedValueOnce("0x0000000000000000000000000000000000000000000000000000000000000004"); // Second poll
|
|
100
|
+
|
|
101
|
+
const localManager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
102
|
+
localManager._generateNonce = jest.fn().mockReturnValue(54321n);
|
|
103
|
+
localManager._calculateChannelId = jest.fn().mockReturnValue("0xCalculatedId");
|
|
104
|
+
|
|
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) });
|
|
113
|
+
|
|
114
|
+
configUtils.notifyAdminGateway.mockResolvedValue();
|
|
115
|
+
|
|
116
|
+
const originalTimeout = global.setTimeout;
|
|
117
|
+
global.setTimeout = (cb) => cb();
|
|
118
|
+
|
|
119
|
+
const receipt = await localManager.deposit(100);
|
|
120
|
+
|
|
121
|
+
expect(receipt.status).toBe(1);
|
|
122
|
+
expect(configUtils.notifyAdminGateway).toHaveBeenCalledWith(
|
|
123
|
+
"ETHEREUM",
|
|
124
|
+
"ETH",
|
|
125
|
+
"0x0000000000000000000000000000000000000001",
|
|
126
|
+
"0xCalculatedId",
|
|
127
|
+
mockHttp
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledTimes(3);
|
|
131
|
+
|
|
132
|
+
global.setTimeout = originalTimeout;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("getAuthToken throws if channel not found after polling (REST)", async () => {
|
|
136
|
+
const mockHttp = jest.fn();
|
|
137
|
+
configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue(null);
|
|
138
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
139
|
+
|
|
140
|
+
const originalTimeout = global.setTimeout;
|
|
141
|
+
global.setTimeout = (cb) => cb();
|
|
142
|
+
|
|
143
|
+
await expect(manager.getAuthToken(100)).rejects.toThrow(/No open payment channel found in Firestore/);
|
|
144
|
+
|
|
145
|
+
global.setTimeout = originalTimeout;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("getAuthToken defaults to channel capacity if amount is null", async () => {
|
|
149
|
+
const mockHttp = jest.fn();
|
|
150
|
+
configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("0x0000000000000000000000000000000000000000000000000000000000000005");
|
|
151
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
152
|
+
|
|
153
|
+
// Mock on-chain response for getChannel
|
|
154
|
+
// Selector (4) + 5 words (5 * 32 bytes = 160 bytes = 320 chars)
|
|
155
|
+
// Amount is word 4 (index 4).
|
|
156
|
+
// 0x + 64*4 chars of padding + 5000 in hex (padded to 64 chars)
|
|
157
|
+
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);
|
|
160
|
+
|
|
161
|
+
mockSigner.getAddress.mockResolvedValue("0x0000000000000000000000000000000000000001");
|
|
162
|
+
mockSigner.signTypedData.mockResolvedValue("0xSignature");
|
|
163
|
+
|
|
164
|
+
const originalTimeout = global.setTimeout;
|
|
165
|
+
global.setTimeout = (cb) => cb();
|
|
166
|
+
|
|
167
|
+
const token = await manager.getAuthToken(); // No amount
|
|
168
|
+
const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
|
|
169
|
+
|
|
170
|
+
expect(decoded.authorized_to_claim).toBe("5000");
|
|
171
|
+
expect(decoded.channel_id).toBe("0x0000000000000000000000000000000000000000000000000000000000000005");
|
|
172
|
+
expect(mockProvider.call).toHaveBeenCalled();
|
|
173
|
+
|
|
174
|
+
global.setTimeout = originalTimeout;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("getAuthToken polls Firestore (REST)", async () => {
|
|
178
|
+
const mockHttp = jest.fn();
|
|
179
|
+
configUtils.retrieveChannelIdFromFirestoreRest
|
|
180
|
+
.mockResolvedValueOnce(null) // First poll
|
|
181
|
+
.mockResolvedValueOnce("0x0000000000000000000000000000000000000000000000000000000000000005"); // Second poll
|
|
182
|
+
|
|
183
|
+
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);
|
|
186
|
+
|
|
187
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
188
|
+
mockSigner.getAddress.mockResolvedValue("0x0000000000000000000000000000000000000001");
|
|
189
|
+
mockSigner.signTypedData.mockResolvedValue("0xSignature");
|
|
190
|
+
|
|
191
|
+
const originalTimeout = global.setTimeout;
|
|
192
|
+
global.setTimeout = (cb) => cb();
|
|
193
|
+
|
|
194
|
+
const token = await manager.getAuthToken(100);
|
|
195
|
+
|
|
196
|
+
expect(token).toBeDefined();
|
|
197
|
+
expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledTimes(2);
|
|
198
|
+
|
|
199
|
+
global.setTimeout = originalTimeout;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("queries Firestore with lowercase address (REST)", async () => {
|
|
203
|
+
const mockHttp = jest.fn();
|
|
204
|
+
const manager = new DhaliEthChannelManager(mockSigner, mockProvider, "ETHEREUM", currency, mockHttp, publicConfig);
|
|
205
|
+
mockSigner.getAddress.mockResolvedValue("0xMixEdCaSeAdDrEsS");
|
|
206
|
+
|
|
207
|
+
await manager._retrieveChannelIdFromFirestore();
|
|
208
|
+
|
|
209
|
+
expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
|
|
210
|
+
"ETHEREUM",
|
|
211
|
+
currency,
|
|
212
|
+
"0xmixedcaseaddress",
|
|
213
|
+
mockHttp
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
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");
|
|
220
|
+
configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("0xRestId");
|
|
221
|
+
|
|
222
|
+
const id = await manager._retrieveChannelIdFromFirestore();
|
|
223
|
+
expect(id).toBe("0xRestId");
|
|
224
|
+
expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
|
|
225
|
+
"ETHEREUM",
|
|
226
|
+
currency,
|
|
227
|
+
"0xmyaddr",
|
|
228
|
+
fetch
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { wrapAsX402PaymentPayload } = require("../src/dhali/utils");
|
|
2
|
+
|
|
3
|
+
describe("wrapAsX402PaymentPayload", () => {
|
|
4
|
+
const mockClaim = {
|
|
5
|
+
version: "2",
|
|
6
|
+
account: "rAccount",
|
|
7
|
+
protocol: "XRPL.MAINNET",
|
|
8
|
+
currency: { code: "XRP", scale: 6 },
|
|
9
|
+
destination_account: "rDest",
|
|
10
|
+
authorized_to_claim: "1000000",
|
|
11
|
+
channel_id: "chan123",
|
|
12
|
+
signature: "sig123",
|
|
13
|
+
};
|
|
14
|
+
const claimBase64 = Buffer.from(JSON.stringify(mockClaim)).toString("base64");
|
|
15
|
+
|
|
16
|
+
const mockReqFull = {
|
|
17
|
+
accepts: [
|
|
18
|
+
{
|
|
19
|
+
scheme: "dhali",
|
|
20
|
+
network: "xrpl:0",
|
|
21
|
+
asset: "xrpl:0/native:xrp",
|
|
22
|
+
amount: "70",
|
|
23
|
+
pay_to: "rLggTEwmTe3eJgyQbCSk4wQazow2TeKrtR",
|
|
24
|
+
max_timeout_seconds: 1209600,
|
|
25
|
+
extra: {},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
const reqFullBase64 = Buffer.from(JSON.stringify(mockReqFull)).toString(
|
|
30
|
+
"base64"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const mockReqDhaliApp = {
|
|
34
|
+
scheme: "dhali",
|
|
35
|
+
network: "eip155:1",
|
|
36
|
+
payTo: "0x3D85634D9EA2854F4276eE5372Bf32Eb4ACDbf77",
|
|
37
|
+
price: {
|
|
38
|
+
amount: "10000000000",
|
|
39
|
+
asset: "eip155:1/erc20:0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD",
|
|
40
|
+
},
|
|
41
|
+
maxTimeoutSeconds: 1209600,
|
|
42
|
+
extra: {},
|
|
43
|
+
};
|
|
44
|
+
const reqDhaliAppBase64 = Buffer.from(JSON.stringify(mockReqDhaliApp)).toString(
|
|
45
|
+
"base64"
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
test("should wrap claim and full requirements correctly", () => {
|
|
49
|
+
const resultBase64 = wrapAsX402PaymentPayload(claimBase64, reqFullBase64);
|
|
50
|
+
const result = JSON.parse(Buffer.from(resultBase64, "base64").toString());
|
|
51
|
+
|
|
52
|
+
expect(result.x402Version).toBe(2);
|
|
53
|
+
expect(result.payload).toEqual(mockClaim);
|
|
54
|
+
expect(result.accepted.amount).toBe("70");
|
|
55
|
+
expect(result.accepted.payTo).toBe("rLggTEwmTe3eJgyQbCSk4wQazow2TeKrtR");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should wrap and normalize Dhali-app style requirements correctly", () => {
|
|
59
|
+
const resultBase64 = wrapAsX402PaymentPayload(claimBase64, reqDhaliAppBase64);
|
|
60
|
+
const result = JSON.parse(Buffer.from(resultBase64, "base64").toString());
|
|
61
|
+
|
|
62
|
+
expect(result.accepted.amount).toBe("10000000000");
|
|
63
|
+
expect(result.accepted.asset).toBe(
|
|
64
|
+
"eip155:1/erc20:0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD"
|
|
65
|
+
);
|
|
66
|
+
expect(result.accepted.payTo).toBe(
|
|
67
|
+
"0x3D85634D9EA2854F4276eE5372Bf32Eb4ACDbf77"
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|