dhali-js 1.0.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.
@@ -0,0 +1,28 @@
1
+ name: Publish to npm
2
+
3
+ # Trigger on any semver tag push (e.g. v1.2.3)
4
+ on:
5
+ push:
6
+ tags:
7
+ - 'v*.*.*'
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v3
15
+
16
+ - name: Set up Node.js
17
+ uses: actions/setup-node@v3
18
+ with:
19
+ node-version: 18.x
20
+ registry-url: https://registry.npmjs.org
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Publish to npm
26
+ run: npm publish --access public
27
+ env:
28
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
@@ -0,0 +1,29 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ node-version: [22.x]
14
+
15
+ steps:
16
+ - name: Checkout repository
17
+ uses: actions/checkout@v3
18
+
19
+ - name: Use Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v3
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: 'npm'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run tests
29
+ run: npm test
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dhali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # dhali-js
2
+
3
+ A JavaScript library for managing XRPL payment channels and generating auth tokens for use with [Dhali](https://dhali.io) APIs.
4
+ Leverages [xrpl.js](https://github.com/XRPLF/xrpl.js) and **only ever performs local signing**—your private key never leaves your environment.
5
+
6
+ ---
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install dhali-js
12
+ ````
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ```js
19
+ import { Wallet, Client } from 'xrpl'
20
+ import {
21
+ ChannelNotFound,
22
+ DhaliChannelManager
23
+ } from 'dhali-js'
24
+
25
+ // 1. Load your wallet from secret
26
+ const seed = 'sXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
27
+ const wallet = Wallet.fromSeed(seed)
28
+
29
+ // 2. Create the manager (connects automatically under the hood)
30
+ const dhaliManager = new DhaliChannelManager(wallet)
31
+
32
+ async function getPaymentClaim() {
33
+ try {
34
+ // Get an auth token
35
+ return await dhaliManager.getAuthToken()
36
+ } catch (err) {
37
+ if (err instanceof ChannelNotFound) {
38
+ // If no channel exists, create one with 1 XRP (1 000 000 drops)
39
+ await dhaliManager.deposit(1_000_000)
40
+ return await dhaliManager.getAuthToken()
41
+ }
42
+ throw err
43
+ }
44
+ }
45
+
46
+ ;(async () => {
47
+ for (let i = 0; i < 2; i++) {
48
+ const token = await getPaymentClaim()
49
+ const url = `https://xrplcluster.dhali.io?payment-claim=${token}`
50
+ // ... use token in your fetch/post to Dhali API ...
51
+ }
52
+ })()
53
+ ```
54
+
55
+ ---
56
+
57
+ ## API
58
+
59
+ ### `new DhaliChannelManager(wallet: xrpl.Wallet)`
60
+
61
+ * **wallet**: an `xrpl.js` `Wallet` instance (e.g. `Wallet.fromSeed`).
62
+
63
+ ---
64
+
65
+ ### `async deposit(amountDrops: number) → Promise<object>`
66
+
67
+ * **amountDrops**: Number of XRP drops (e.g. `1_000_000` = 1 XRP).
68
+ * **Returns**: The JSON result of the `PaymentChannelCreate` or `PaymentChannelFund` transaction.
69
+
70
+ ---
71
+
72
+ ### `async getAuthToken(amountDrops?: number) → Promise<string>`
73
+
74
+ * **amountDrops** (optional): How many drops to authorize; defaults to full channel balance.
75
+ * **Returns**: A base64-encoded JSON string containing your signed claim.
76
+ * **Throws**:
77
+
78
+ * `ChannelNotFound` if there is no open channel.
79
+ * `Error` if `amountDrops` exceeds channel capacity.
80
+
81
+ ---
82
+
83
+ ## Errors
84
+
85
+ * **ChannelNotFound**
86
+ Thrown when `getAuthToken` finds no channel from your wallet to Dhali’s receiver.
87
+
88
+ ---
89
+
90
+ ## Security
91
+
92
+ All XRPL interactions and claim-signatures are done locally via `xrpl.js` + `ripple-keypairs`.
93
+ Your private key never leaves your machine.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "dhali-js",
3
+ "version": "1.0.0",
4
+ "description": "A JavaScript library for managing XRPL payment channels and generating auth tokens for Dhali APIs",
5
+ "main": "src/index.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "test": "jest",
9
+ "format": "prettier --write \"src/**/*.js\" \"tests/**/*.js\""
10
+ },
11
+ "keywords": [
12
+ "xrpl",
13
+ "payment-channel",
14
+ "dhali",
15
+ "xrpl.js"
16
+ ],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "ripple-keypairs": "^2.0.0",
20
+ "xrpl": "^4.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "jest": "^29.0.0",
24
+ "prettier": "^3.5.3"
25
+ }
26
+ }
@@ -0,0 +1,110 @@
1
+ const { Client } = require("xrpl");
2
+ const { buildPaychanAuthHexStringToBeSigned } = require("./createSignedClaim");
3
+ const { sign: signClaim } = require("ripple-keypairs");
4
+
5
+ class ChannelNotFound extends Error {}
6
+
7
+ /**
8
+ * A management tool for generating payment claims for use with Dhali APIs.
9
+ */
10
+ class DhaliChannelManager {
11
+ /**
12
+ * @param {xrpl.Wallet} wallet
13
+ */
14
+ constructor(wallet) {
15
+ this.client = new Client("wss://s1.ripple.com:51234/");
16
+ this.ready = this.client.connect();
17
+ this.wallet = wallet;
18
+ this.destination = "rLggTEwmTe3eJgyQbCSk4wQazow2TeKrtR";
19
+ this.protocol = "XRPL.MAINNET";
20
+ }
21
+
22
+ async _findChannel() {
23
+ await this.ready;
24
+ const resp = await this.client.request({
25
+ command: "account_channels",
26
+ account: this.wallet.classicAddress,
27
+ destination_account: this.destination,
28
+ ledger_index: "validated",
29
+ });
30
+ const channels = resp.result.channels || [];
31
+ if (channels.length === 0) {
32
+ throw new ChannelNotFound(
33
+ `No open payment channel from ${this.wallet.classicAddress} to ${this.destination}`,
34
+ );
35
+ }
36
+ return channels[0];
37
+ }
38
+
39
+ /**
40
+ * Create or fund a payment channel.
41
+ * @param {number} amountDrops
42
+ * @returns {Promise<object>}
43
+ */
44
+ async deposit(amountDrops) {
45
+ await this.ready;
46
+ let tx;
47
+ try {
48
+ const ch = await this._findChannel();
49
+ tx = {
50
+ TransactionType: "PaymentChannelFund",
51
+ Account: this.wallet.classicAddress,
52
+ Channel: ch.channel_id,
53
+ Amount: amountDrops.toString(),
54
+ };
55
+ } catch (err) {
56
+ if (!(err instanceof ChannelNotFound)) throw err;
57
+ tx = {
58
+ TransactionType: "PaymentChannelCreate",
59
+ Account: this.wallet.classicAddress,
60
+ Destination: this.destination,
61
+ Amount: amountDrops.toString(),
62
+ SettleDelay: 86400 * 14,
63
+ PublicKey: this.wallet.publicKey,
64
+ };
65
+ }
66
+ // autofill sequence, fee, etc.
67
+ const prepared = await this.client.autofill(tx);
68
+ // sign
69
+ const signed = this.wallet.sign(prepared);
70
+ const txBlob = signed.tx_blob || signed.signedTransaction;
71
+ // submit & wait
72
+ const result = await this.client.submitAndWait(txBlob);
73
+ return result.result;
74
+ }
75
+
76
+ /**
77
+ * Generate a base64-encoded payment claim.
78
+ * @param {number=} amountDrops
79
+ * @returns {Promise<string>}
80
+ */
81
+ async getAuthToken(amountDrops) {
82
+ await this.ready;
83
+ const ch = await this._findChannel();
84
+ const total = BigInt(ch.amount);
85
+ const allowed = amountDrops != null ? BigInt(amountDrops) : total;
86
+ if (allowed > total) {
87
+ throw new Error(
88
+ `Requested auth ${allowed} exceeds channel capacity ${total}`,
89
+ );
90
+ }
91
+ const claimHex = buildPaychanAuthHexStringToBeSigned(
92
+ ch.channel_id,
93
+ allowed.toString(),
94
+ );
95
+ const signature = signClaim(claimHex, this.wallet.privateKey);
96
+ const claim = {
97
+ version: "2",
98
+ account: this.wallet.classicAddress,
99
+ protocol: this.protocol,
100
+ currency: { code: "XRP", scale: 6 },
101
+ destination_account: this.destination,
102
+ authorized_to_claim: allowed.toString(),
103
+ channel_id: ch.channel_id,
104
+ signature,
105
+ };
106
+ return Buffer.from(JSON.stringify(claim)).toString("base64");
107
+ }
108
+ }
109
+
110
+ module.exports = { DhaliChannelManager, ChannelNotFound };
@@ -0,0 +1,66 @@
1
+ const HASH_PREFIX_PAYMENT_CHANNEL_CLAIM = 0x434c4d00;
2
+
3
+ /**
4
+ * @private
5
+ */
6
+ function _serializePaychanAuthorization(channelIdBytes, dropsBigInt) {
7
+ if (!Buffer.isBuffer(channelIdBytes) || channelIdBytes.length !== 32) {
8
+ throw new Error(
9
+ `Invalid channelId length ${channelIdBytes.length}; must be 32 bytes.`,
10
+ );
11
+ }
12
+ // 1) 4-byte prefix
13
+ const prefix = Buffer.alloc(4);
14
+ prefix.writeUInt32BE(HASH_PREFIX_PAYMENT_CHANNEL_CLAIM, 0);
15
+
16
+ // 2) channelIdBytes (32 bytes)
17
+ // 3) split drops into two 4-byte words
18
+ const highBig = dropsBigInt >> 32n;
19
+ const lowBig = dropsBigInt & 0xffffffffn;
20
+ const high = Number(highBig);
21
+ const low = Number(lowBig);
22
+ const amountBuf = Buffer.alloc(8);
23
+ amountBuf.writeUInt32BE(high, 0);
24
+ amountBuf.writeUInt32BE(low, 4);
25
+
26
+ return Buffer.concat([prefix, channelIdBytes, amountBuf]);
27
+ }
28
+
29
+ /**
30
+ * Build the hex-string that you pass to an XRPL signer to sign a payment-channel claim.
31
+ *
32
+ * @param {string} channelIdHex 64-char hex string
33
+ * @param {string} amountStr integer string of drops
34
+ * @returns {string} uppercase hex
35
+ */
36
+ function buildPaychanAuthHexStringToBeSigned(channelIdHex, amountStr) {
37
+ let channelIdBytes;
38
+
39
+ channelIdBytes = Buffer.from(channelIdHex, "hex");
40
+
41
+ if (channelIdBytes.length !== 32) {
42
+ throw new Error(
43
+ `Invalid channelId length ${channelIdBytes.length}; must be 32 bytes.`,
44
+ );
45
+ }
46
+
47
+ let dropsBig;
48
+ try {
49
+ dropsBig = BigInt(amountStr);
50
+ } catch {
51
+ throw new Error("Invalid amount format.");
52
+ }
53
+
54
+ if (dropsBig < 0n) {
55
+ throw new Error("Amount cannot be negative.");
56
+ }
57
+
58
+ const msg = _serializePaychanAuthorization(channelIdBytes, dropsBig);
59
+ return msg.toString("hex").toUpperCase();
60
+ }
61
+
62
+ module.exports = {
63
+ buildPaychanAuthHexStringToBeSigned,
64
+ // exposed for testing
65
+ _serializePaychanAuthorization,
66
+ };
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ const {
2
+ buildPaychanAuthHexStringToBeSigned,
3
+ _serializePaychanAuthorization,
4
+ } = require("./dhali/createSignedClaim");
5
+ const {
6
+ DhaliChannelManager,
7
+ ChannelNotFound,
8
+ } = require("./dhali/DhaliChannelManager");
9
+
10
+ module.exports = {
11
+ buildPaychanAuthHexStringToBeSigned,
12
+ _serializePaychanAuthorization,
13
+ DhaliChannelManager,
14
+ ChannelNotFound,
15
+ };
@@ -0,0 +1,220 @@
1
+ // tests/DhaliChannelManager.test.js
2
+
3
+ // 1) MOCK xrpl.js BEFORE importing any code that instantiates Client
4
+ jest.mock("xrpl", () => {
5
+ // Return an object with the Client class stubbed
6
+ return {
7
+ Client: jest.fn().mockImplementation(() => {
8
+ return {
9
+ // connect() returns a resolved Promise so await this.ready never hangs
10
+ connect: () => Promise.resolve(),
11
+ // stubbed methods your code calls:
12
+ request: jest.fn(),
13
+ autofill: jest.fn(),
14
+ submitAndWait: jest.fn(),
15
+ disconnect: jest.fn(),
16
+ // preserve .url for the constructor test
17
+ url: "wss://s1.ripple.com:51234/",
18
+ };
19
+ }),
20
+ };
21
+ });
22
+
23
+ // 2) MOCK ripple-keypairs BEFORE importing DhaliChannelManager
24
+ jest.mock("ripple-keypairs", () => ({
25
+ // We'll override this mock's behavior inside individual tests
26
+ sign: jest.fn(),
27
+ }));
28
+
29
+ jest.mock("../src/dhali/createSignedClaim", () => ({
30
+ buildPaychanAuthHexStringToBeSigned: jest.fn(),
31
+ // If you need to expose _serializePaychanAuthorization, add it here too:
32
+ // _serializePaychanAuthorization: jest.fn(),
33
+ }));
34
+
35
+ // 3) Now import everything under test
36
+ const createSignedClaim = require("../src/dhali/createSignedClaim");
37
+ const { sign: mockRippleSign } = require("ripple-keypairs");
38
+ const {
39
+ DhaliChannelManager,
40
+ ChannelNotFound,
41
+ } = require("../src/dhali/DhaliChannelManager");
42
+
43
+ describe("DhaliChannelManager", () => {
44
+ const CHANNEL_ID = "AB".repeat(32);
45
+ let wallet;
46
+ let manager;
47
+
48
+ beforeEach(() => {
49
+ // 4) Create a minimal fake wallet
50
+ wallet = {
51
+ classicAddress: "rTESTADDRESS",
52
+ publicKey: "PUBKEY",
53
+ privateKey: "PRIVKEY",
54
+ // sign() is used for transaction signing
55
+ sign: jest.fn().mockReturnValue({ signedTransaction: "TX_BLOB" }),
56
+ };
57
+ // 5) Instantiate the manager. client.connect() is already stubbed.
58
+ manager = new DhaliChannelManager(wallet);
59
+ // 6) Short-circuit any `await this.ready` if your constructor awaits client.connect()
60
+ manager.ready = Promise.resolve();
61
+ });
62
+
63
+ afterEach(() => {
64
+ // 7) Restore any spies on createSignedClaim so tests remain isolated
65
+ if (createSignedClaim.buildPaychanAuthHexStringToBeSigned.mockRestore) {
66
+ createSignedClaim.buildPaychanAuthHexStringToBeSigned.mockRestore();
67
+ }
68
+ });
69
+
70
+ test("constructor sets defaults", () => {
71
+ expect(manager.wallet).toBe(wallet);
72
+ expect(manager.protocol).toBe("XRPL.MAINNET");
73
+ expect(manager.destination).toBe("rLggTEwmTe3eJgyQbCSk4wQazow2TeKrtR");
74
+ // client.url comes from our mock above
75
+ expect(manager.client.url).toBe("wss://s1.ripple.com:51234/");
76
+ });
77
+
78
+ describe("_findChannel", () => {
79
+ test("returns first channel when present", async () => {
80
+ const fakeChannel = { channel_id: "CHAN123", amount: "1000" };
81
+ manager.client.request.mockResolvedValue({
82
+ result: { channels: [fakeChannel] },
83
+ });
84
+
85
+ const ch = await manager._findChannel();
86
+ expect(ch).toBe(fakeChannel);
87
+ // verify the exact request payload
88
+ expect(manager.client.request).toHaveBeenCalledWith({
89
+ command: "account_channels",
90
+ account: wallet.classicAddress,
91
+ destination_account: manager.destination,
92
+ ledger_index: "validated",
93
+ });
94
+ });
95
+
96
+ test("throws ChannelNotFound when none", async () => {
97
+ manager.client.request.mockResolvedValue({ result: { channels: [] } });
98
+
99
+ await expect(manager._findChannel()).rejects.toThrow(ChannelNotFound);
100
+ await expect(manager._findChannel()).rejects.toThrow(
101
+ wallet.classicAddress,
102
+ );
103
+ await expect(manager._findChannel()).rejects.toThrow(manager.destination);
104
+ });
105
+ });
106
+
107
+ describe("deposit", () => {
108
+ test("funds existing channel", async () => {
109
+ // _findChannel resolves => fund path
110
+ const fakeChannel = { channel_id: "CHANID", amount: "500" };
111
+ manager._findChannel = jest.fn().mockResolvedValue(fakeChannel);
112
+
113
+ // stub autofill + submitAndWait from our Client mock
114
+ manager.client.autofill.mockResolvedValue({ foo: "bar" });
115
+ manager.client.submitAndWait.mockResolvedValue({
116
+ result: { status: "funded" },
117
+ });
118
+
119
+ const res = await manager.deposit(100);
120
+ expect(res).toEqual({ status: "funded" });
121
+
122
+ // ensure autofill got the correct PaymentChannelFund payload
123
+ expect(manager.client.autofill).toHaveBeenCalledWith({
124
+ TransactionType: "PaymentChannelFund",
125
+ Account: wallet.classicAddress,
126
+ Channel: fakeChannel.channel_id,
127
+ Amount: "100",
128
+ });
129
+ // ensure we submitted the signed blob returned by wallet.sign()
130
+ expect(manager.client.submitAndWait).toHaveBeenCalledWith("TX_BLOB");
131
+ });
132
+
133
+ test("creates channel if none exists", async () => {
134
+ // _findChannel rejects with ChannelNotFound => create path
135
+ manager._findChannel = jest
136
+ .fn()
137
+ .mockRejectedValue(new ChannelNotFound("nope"));
138
+
139
+ manager.client.autofill.mockResolvedValue({ baz: "qux" });
140
+ manager.client.submitAndWait.mockResolvedValue({
141
+ result: { status: "created" },
142
+ });
143
+
144
+ const res = await manager.deposit(200);
145
+ expect(res).toEqual({ status: "created" });
146
+
147
+ expect(manager.client.autofill).toHaveBeenCalledWith({
148
+ TransactionType: "PaymentChannelCreate",
149
+ Account: wallet.classicAddress,
150
+ Destination: manager.destination,
151
+ Amount: "200",
152
+ SettleDelay: 86400 * 14,
153
+ PublicKey: wallet.publicKey,
154
+ });
155
+ expect(manager.client.submitAndWait).toHaveBeenCalledWith("TX_BLOB");
156
+ });
157
+ });
158
+
159
+ describe("getAuthToken", () => {
160
+ test("success with default amount", async () => {
161
+ // stub channel lookup
162
+ manager._findChannel = jest.fn().mockResolvedValue({
163
+ channel_id: CHANNEL_ID,
164
+ amount: "1001",
165
+ });
166
+
167
+ // 8) SPY on the hex-builder instead of reassigning it
168
+ const claimSpy = jest
169
+ .spyOn(createSignedClaim, "buildPaychanAuthHexStringToBeSigned")
170
+ .mockReturnValue("CLAIMHEX");
171
+
172
+ // stub signature
173
+ mockRippleSign.mockReturnValue("SIGVALUE");
174
+
175
+ const token = await manager.getAuthToken();
176
+ const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
177
+
178
+ expect(decoded).toMatchObject({
179
+ version: "2",
180
+ account: wallet.classicAddress,
181
+ protocol: manager.protocol,
182
+ currency: { code: "XRP", scale: 6 },
183
+ destination_account: manager.destination,
184
+ authorized_to_claim: "1001",
185
+ channel_id: CHANNEL_ID,
186
+ signature: "SIGVALUE",
187
+ });
188
+
189
+ // ensure our spy was called with correct args
190
+ expect(claimSpy).toHaveBeenCalledWith(CHANNEL_ID, "1001");
191
+ });
192
+
193
+ test("success with specific amount", async () => {
194
+ manager._findChannel = jest.fn().mockResolvedValue({
195
+ channel_id: CHANNEL_ID,
196
+ amount: "500",
197
+ });
198
+
199
+ const claimSpy = jest
200
+ .spyOn(createSignedClaim, "buildPaychanAuthHexStringToBeSigned")
201
+ .mockReturnValue("CLAIMHEX2");
202
+ mockRippleSign.mockReturnValue("SIG2");
203
+
204
+ const token = await manager.getAuthToken(200);
205
+ const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
206
+ expect(decoded.authorized_to_claim).toBe("200");
207
+ expect(claimSpy).toHaveBeenCalledWith(CHANNEL_ID, "200");
208
+ });
209
+
210
+ test("throws if amount exceeds capacity", async () => {
211
+ manager._findChannel = jest.fn().mockResolvedValue({
212
+ channel_id: CHANNEL_ID,
213
+ amount: "100",
214
+ });
215
+ await expect(manager.getAuthToken(200)).rejects.toThrow(
216
+ /exceeds channel capacity/,
217
+ );
218
+ });
219
+ });
220
+ });
@@ -0,0 +1,57 @@
1
+ const {
2
+ _serializePaychanAuthorization,
3
+ buildPaychanAuthHexStringToBeSigned,
4
+ } = require("../src/dhali/createSignedClaim");
5
+
6
+ describe("_serializePaychanAuthorization", () => {
7
+ test("throws if channelIdBytes length is not 32", () => {
8
+ expect(() =>
9
+ _serializePaychanAuthorization(Buffer.alloc(31), 1000n),
10
+ ).toThrow("Invalid channelId length 31; must be 32 bytes.");
11
+ });
12
+
13
+ test("serializes correctly", () => {
14
+ // 32 bytes of 0xAA
15
+ const channelIdBytes = Buffer.alloc(32, 0xaa);
16
+ // a 64-bit drops value
17
+ const drops = 0x1122334455667788n;
18
+
19
+ const buf = _serializePaychanAuthorization(channelIdBytes, drops);
20
+
21
+ // prefix = 0x434C4D00
22
+ expect(buf.slice(0, 4).toString("hex")).toBe("434c4d00");
23
+ // next 32 bytes are the channelId
24
+ expect(buf.slice(4, 4 + 32)).toEqual(channelIdBytes);
25
+ // final 8 bytes = high + low words
26
+ expect(buf.slice(36).toString("hex")).toBe("1122334455667788");
27
+ });
28
+ });
29
+
30
+ describe("buildPaychanAuthHexStringToBeSigned", () => {
31
+ const zeroHex64 = "00".repeat(32);
32
+
33
+ test("negative amount throws", () => {
34
+ expect(() => buildPaychanAuthHexStringToBeSigned(zeroHex64, "-1")).toThrow(
35
+ "Amount cannot be negative.",
36
+ );
37
+ });
38
+
39
+ test("non-numeric amount throws", () => {
40
+ expect(() => buildPaychanAuthHexStringToBeSigned(zeroHex64, "foo")).toThrow(
41
+ "Invalid amount format.",
42
+ );
43
+ });
44
+
45
+ test("produces correct uppercase hex", () => {
46
+ const channelHex = "AA".repeat(32);
47
+ const amountStr = "123456";
48
+ const hexStr = buildPaychanAuthHexStringToBeSigned(channelHex, amountStr);
49
+ const expected = _serializePaychanAuthorization(
50
+ Buffer.from(channelHex, "hex"),
51
+ BigInt(amountStr),
52
+ )
53
+ .toString("hex")
54
+ .toUpperCase();
55
+ expect(hexStr).toBe(expected);
56
+ });
57
+ });