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,203 @@
|
|
|
1
|
+
const Currency = require("./Currency");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} NetworkCurrencyConfig
|
|
5
|
+
* @property {Currency} currency
|
|
6
|
+
* @property {string} destinationAddress
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetches and parses available Dhali currencies and configurations.
|
|
11
|
+
* @returns {Promise<Object.<string, Object.<string, NetworkCurrencyConfig>>>}
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @param {typeof fetch} [httpClient]
|
|
15
|
+
* @returns {Promise<Object>}
|
|
16
|
+
*/
|
|
17
|
+
async function getAvailableDhaliCurrencies(httpClient = fetch) {
|
|
18
|
+
const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
|
|
19
|
+
let data;
|
|
20
|
+
try {
|
|
21
|
+
const response = await httpClient(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
data = await response.json();
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(`Failed to fetch Dhali configuration: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const publicAddresses = data.DHALI_PUBLIC_ADDRESSES || {};
|
|
31
|
+
/** @type {Object.<string, Object.<string, NetworkCurrencyConfig>>} */
|
|
32
|
+
const result = {};
|
|
33
|
+
|
|
34
|
+
for (const [network, currencies] of Object.entries(publicAddresses)) {
|
|
35
|
+
result[network] = {};
|
|
36
|
+
for (const [code, details] of Object.entries(currencies)) {
|
|
37
|
+
const tokenAddress = details.issuer || null;
|
|
38
|
+
const scale = details.scale || 6;
|
|
39
|
+
const destination = details.wallet_id;
|
|
40
|
+
|
|
41
|
+
if (!destination) continue;
|
|
42
|
+
|
|
43
|
+
const curr = new Currency(code, scale, tokenAddress);
|
|
44
|
+
|
|
45
|
+
result[network][code] = {
|
|
46
|
+
currency: curr,
|
|
47
|
+
destinationAddress: destination
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetches the raw Dhali public configuration JSON.
|
|
56
|
+
* @returns {Promise<Object>}
|
|
57
|
+
*/
|
|
58
|
+
async function fetchPublicConfig(httpClient = fetch) {
|
|
59
|
+
const url = "https://raw.githubusercontent.com/Dhali-org/Dhali-config/master/public.prod.json";
|
|
60
|
+
try {
|
|
61
|
+
const response = await httpClient(url);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
return await response.json();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw new Error(`Failed to fetch Dhali configuration: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Proactively notifies the Dhali Admin Gateway about a new payment channel.
|
|
73
|
+
* @param {string} protocol
|
|
74
|
+
* @param {string} currencyIdentifier
|
|
75
|
+
* @param {string} accountAddress
|
|
76
|
+
* @param {string} channelId
|
|
77
|
+
* @param {typeof fetch} [httpClient]
|
|
78
|
+
*/
|
|
79
|
+
async function notifyAdminGateway(protocol, currencyIdentifier, accountAddress, channelId, httpClient = fetch) {
|
|
80
|
+
const config = await fetchPublicConfig(httpClient);
|
|
81
|
+
const rootUrl = config.ROOT_API_ADMIN_URL;
|
|
82
|
+
if (!rootUrl) return;
|
|
83
|
+
|
|
84
|
+
const httpRootUrl = rootUrl.replace("wss://", "https://").replace("ws://", "http://");
|
|
85
|
+
const url = `${httpRootUrl}/public_claim_info/${protocol}/${currencyIdentifier}`;
|
|
86
|
+
|
|
87
|
+
if (!channelId.startsWith("0x")) {
|
|
88
|
+
channelId = "0x" + channelId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const payload = {
|
|
92
|
+
account: accountAddress,
|
|
93
|
+
channel_id: channelId
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await httpClient(url, {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json"
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(payload)
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Best effort notification
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
const FIREBASE_CONFIG = {
|
|
112
|
+
apiKey: "AIzaSyBro8QN3zyJwyo92lYUMPwsyRVPLLGOTcs",
|
|
113
|
+
authDomain: "dhali-prod.firebaseapp.com",
|
|
114
|
+
projectId: "dhali-prod",
|
|
115
|
+
storageBucket: "dhali-prod.firebasestorage.app",
|
|
116
|
+
messagingSenderId: "1042340549063",
|
|
117
|
+
appId: "1:1042340549063:web:3dc69cffe6d3c0746189e2",
|
|
118
|
+
measurementId: "G-6TPZFK7NQ6",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Queries Firestore via REST API using the public API key.
|
|
123
|
+
* @param {string} protocol
|
|
124
|
+
* @param {import("./Currency")} currency
|
|
125
|
+
* @param {string} accountAddress
|
|
126
|
+
* @param {typeof fetch} [httpClient]
|
|
127
|
+
* @returns {Promise<string|null>}
|
|
128
|
+
*/
|
|
129
|
+
async function retrieveChannelIdFromFirestoreRest(protocol, currency, accountAddress, httpClient = fetch) {
|
|
130
|
+
let currencyIdentifier = currency.code;
|
|
131
|
+
if (currency.tokenAddress) {
|
|
132
|
+
currencyIdentifier = `${currency.code}.${currency.tokenAddress}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const projectId = FIREBASE_CONFIG.projectId;
|
|
136
|
+
const apiKey = FIREBASE_CONFIG.apiKey;
|
|
137
|
+
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/public_claim_info/${protocol}:runQuery?key=${apiKey}`;
|
|
138
|
+
|
|
139
|
+
const query = {
|
|
140
|
+
structuredQuery: {
|
|
141
|
+
from: [{ collectionId: currencyIdentifier }],
|
|
142
|
+
where: {
|
|
143
|
+
compositeFilter: {
|
|
144
|
+
op: "AND",
|
|
145
|
+
filters: [
|
|
146
|
+
{
|
|
147
|
+
fieldFilter: {
|
|
148
|
+
field: { fieldPath: "account" },
|
|
149
|
+
op: "EQUAL",
|
|
150
|
+
value: { stringValue: accountAddress },
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
fieldFilter: {
|
|
155
|
+
field: { fieldPath: "closed" },
|
|
156
|
+
op: "NOT_EQUAL",
|
|
157
|
+
value: { booleanValue: true },
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const response = await httpClient(url, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: {
|
|
170
|
+
"Content-Type": "application/json"
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify(query)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!response.ok) return null;
|
|
176
|
+
|
|
177
|
+
const results = await response.json();
|
|
178
|
+
|
|
179
|
+
for (const result of results) {
|
|
180
|
+
const doc = result.document;
|
|
181
|
+
if (!doc) continue;
|
|
182
|
+
|
|
183
|
+
const fields = doc.fields || {};
|
|
184
|
+
const closing = fields.closing ? fields.closing.booleanValue : false;
|
|
185
|
+
const closed = fields.closed ? fields.closed.booleanValue : false;
|
|
186
|
+
|
|
187
|
+
if (closing || closed) continue;
|
|
188
|
+
|
|
189
|
+
const channelId = fields.channel_id ? fields.channel_id.stringValue : null;
|
|
190
|
+
if (channelId) return channelId;
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
getAvailableDhaliCurrencies,
|
|
200
|
+
fetchPublicConfig,
|
|
201
|
+
notifyAdminGateway,
|
|
202
|
+
retrieveChannelIdFromFirestoreRest
|
|
203
|
+
};
|
|
@@ -59,8 +59,42 @@ function buildPaychanAuthHexStringToBeSigned(channelIdHex, amountStr) {
|
|
|
59
59
|
return msg.toString("hex").toUpperCase();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get Typed Data structure for EIP-712 signing
|
|
64
|
+
*/
|
|
65
|
+
function getEthereumClaimTypedData(channelId, tokenAddress, maxAmount, chainId, contractAddress) {
|
|
66
|
+
const domain = {
|
|
67
|
+
name: "DhaliPaymentChannel",
|
|
68
|
+
version: "2.1.0",
|
|
69
|
+
chainId: chainId,
|
|
70
|
+
verifyingContract: contractAddress,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const types = {
|
|
74
|
+
DhaliClaim: [
|
|
75
|
+
{ name: "channelId", type: "bytes32" },
|
|
76
|
+
{ name: "token", type: "address" },
|
|
77
|
+
{ name: "maxAmount", type: "uint256" },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (typeof channelId === "string" && !channelId.startsWith("0x")) {
|
|
82
|
+
channelId = "0x" + channelId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const value = {
|
|
86
|
+
channelId: channelId,
|
|
87
|
+
token: tokenAddress,
|
|
88
|
+
maxAmount: maxAmount
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return { domain, types, value };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
62
95
|
module.exports = {
|
|
63
96
|
buildPaychanAuthHexStringToBeSigned,
|
|
64
97
|
// exposed for testing
|
|
65
98
|
_serializePaychanAuthorization,
|
|
99
|
+
getEthereumClaimTypedData
|
|
66
100
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a base64 encoded claim and a base64 encoded requirement into an x402 compliant payload.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} claimBase64 - The base64 encoded claim object.
|
|
5
|
+
* @param {string} paymentRequirementBase64 - The base64 encoded payment requirement object.
|
|
6
|
+
* @returns {string} The base64 encoded x402 payment payload.
|
|
7
|
+
*/
|
|
8
|
+
function wrapAsX402PaymentPayload(claimBase64, paymentRequirementBase64) {
|
|
9
|
+
const decodedClaim = JSON.parse(
|
|
10
|
+
Buffer.from(claimBase64, "base64").toString("utf-8")
|
|
11
|
+
);
|
|
12
|
+
let req = JSON.parse(
|
|
13
|
+
Buffer.from(paymentRequirementBase64, "base64").toString("utf-8")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (req.accepts) {
|
|
17
|
+
req = Array.isArray(req.accepts) ? req.accepts[0] : req.accepts;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Normalize fields to match Dhali-wallet's PaymentRequirements defaults (camelCase)
|
|
21
|
+
const normalizedReq = {
|
|
22
|
+
scheme: req.scheme || "",
|
|
23
|
+
network: req.network || "",
|
|
24
|
+
asset: req.asset || (req.price ? req.price.asset : "") || "",
|
|
25
|
+
amount: String(req.amount || (req.price ? req.price.amount : "0")),
|
|
26
|
+
payTo: req.payTo || req.pay_to || "",
|
|
27
|
+
maxTimeoutSeconds: parseInt(
|
|
28
|
+
req.maxTimeoutSeconds || req.max_timeout_seconds || 1209600
|
|
29
|
+
),
|
|
30
|
+
extra: req.extra || {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const x402Payload = {
|
|
34
|
+
x402Version: 2,
|
|
35
|
+
payload: decodedClaim,
|
|
36
|
+
accepted: normalizedReq,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return Buffer.from(JSON.stringify(x402Payload)).toString("base64");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
wrapAsX402PaymentPayload,
|
|
44
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} = require("./dhali/
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
ChannelNotFound,
|
|
8
|
-
} = require("./dhali/DhaliChannelManager");
|
|
1
|
+
const { DhaliChannelManager } = require("./dhali/DhaliChannelManager");
|
|
2
|
+
const { DhaliXrplChannelManager, ChannelNotFound } = require("./dhali/DhaliXrplChannelManager");
|
|
3
|
+
const { DhaliEthChannelManager } = require("./dhali/DhaliEthChannelManager");
|
|
4
|
+
const { Currency } = require("./dhali/Currency");
|
|
5
|
+
const { getAvailableDhaliCurrencies } = require("./dhali/configUtils");
|
|
6
|
+
const { wrapAsX402PaymentPayload } = require("./dhali/utils");
|
|
9
7
|
|
|
10
8
|
module.exports = {
|
|
11
|
-
buildPaychanAuthHexStringToBeSigned,
|
|
12
|
-
_serializePaychanAuthorization,
|
|
13
9
|
DhaliChannelManager,
|
|
10
|
+
DhaliXrplChannelManager,
|
|
11
|
+
DhaliEthChannelManager,
|
|
14
12
|
ChannelNotFound,
|
|
13
|
+
Currency,
|
|
14
|
+
getAvailableDhaliCurrencies,
|
|
15
|
+
wrapAsX402PaymentPayload
|
|
15
16
|
};
|
|
@@ -1,193 +1,128 @@
|
|
|
1
|
-
|
|
1
|
+
const {
|
|
2
|
+
DhaliXrplChannelManager,
|
|
3
|
+
ChannelNotFound,
|
|
4
|
+
} = require("../src/dhali/DhaliXrplChannelManager");
|
|
5
|
+
const createSignedClaim = require("../src/dhali/createSignedClaim");
|
|
6
|
+
const rippleKeypairs = require("ripple-keypairs");
|
|
7
|
+
const Currency = require("../src/dhali/Currency");
|
|
2
8
|
|
|
3
|
-
// 1) MOCK xrpl.js BEFORE importing any code that instantiates Client
|
|
4
9
|
jest.mock("xrpl", () => {
|
|
5
|
-
// Return an object with the Client class stubbed
|
|
6
10
|
return {
|
|
7
|
-
Client: jest.fn().mockImplementation(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// preserve .url for the constructor test
|
|
17
|
-
url: "wss://s1.ripple.com:51234/",
|
|
18
|
-
};
|
|
19
|
-
}),
|
|
11
|
+
Client: jest.fn().mockImplementation(() => ({
|
|
12
|
+
connect: jest.fn().mockResolvedValue(),
|
|
13
|
+
request: jest.fn(),
|
|
14
|
+
submitAndWait: jest.fn(),
|
|
15
|
+
autofill: jest.fn().mockResolvedValue({}),
|
|
16
|
+
})),
|
|
17
|
+
Wallet: {
|
|
18
|
+
fromSeed: jest.fn(),
|
|
19
|
+
},
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
// 2) MOCK ripple-keypairs BEFORE importing DhaliChannelManager
|
|
24
23
|
jest.mock("ripple-keypairs", () => ({
|
|
25
|
-
// We'll override this mock's behavior inside individual tests
|
|
26
24
|
sign: jest.fn(),
|
|
27
25
|
}));
|
|
28
26
|
|
|
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
27
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} = require("../src/dhali/DhaliChannelManager");
|
|
28
|
+
|
|
29
|
+
const configUtils = require("../src/dhali/configUtils");
|
|
30
|
+
jest.mock("../src/dhali/configUtils", () => ({
|
|
31
|
+
fetchPublicConfig: jest.fn().mockResolvedValue({}),
|
|
32
|
+
retrieveChannelIdFromFirestoreRest: jest.fn(),
|
|
33
|
+
}));
|
|
42
34
|
|
|
43
35
|
describe("DhaliChannelManager", () => {
|
|
44
|
-
const CHANNEL_ID = "AB".repeat(32);
|
|
45
|
-
let wallet;
|
|
46
36
|
let manager;
|
|
37
|
+
let mockClient;
|
|
38
|
+
let wallet;
|
|
39
|
+
let currency;
|
|
40
|
+
const CHANNEL_ID = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
47
41
|
|
|
48
42
|
beforeEach(() => {
|
|
49
|
-
// 4) Create a minimal fake wallet
|
|
50
43
|
wallet = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
sign: jest.fn().mockReturnValue({
|
|
44
|
+
address: "rTestAddress",
|
|
45
|
+
classicAddress: "rTestAddress",
|
|
46
|
+
publicKey: "TEST_PUB_KEY",
|
|
47
|
+
privateKey: "TEST_PRIV_KEY",
|
|
48
|
+
sign: jest.fn().mockReturnValue({ tx_blob: "BLOB" }),
|
|
56
49
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
mockClient = {
|
|
51
|
+
connect: jest.fn().mockResolvedValue(),
|
|
52
|
+
request: jest.fn(),
|
|
53
|
+
submitAndWait: jest.fn(),
|
|
54
|
+
autofill: jest.fn().mockResolvedValue({}),
|
|
55
|
+
};
|
|
56
|
+
currency = new Currency("XRP", 6);
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
createSignedClaim.buildPaychanAuthHexStringToBeSigned.mockRestore();
|
|
67
|
-
}
|
|
58
|
+
const mockHttp = jest.fn();
|
|
59
|
+
configUtils.retrieveChannelIdFromFirestoreRest.mockResolvedValue("CHAN123");
|
|
60
|
+
manager = new DhaliXrplChannelManager(wallet, mockClient, "XRPL.MAINNET", currency, mockHttp);
|
|
68
61
|
});
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
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/");
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
jest.clearAllMocks();
|
|
76
65
|
});
|
|
77
66
|
|
|
78
|
-
describe("
|
|
79
|
-
test("
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
result: {
|
|
67
|
+
describe("deposit", () => {
|
|
68
|
+
test("funds existing channel if found", async () => {
|
|
69
|
+
mockClient.rpc_client = { request: jest.fn() }; // not really how it works but let's see
|
|
70
|
+
mockClient.request.mockResolvedValue({
|
|
71
|
+
result: {
|
|
72
|
+
channels: [{ channel_id: CHANNEL_ID }],
|
|
73
|
+
},
|
|
83
74
|
});
|
|
75
|
+
mockClient.submitAndWait.mockResolvedValue({ result: "SUCCESS" });
|
|
76
|
+
mockClient.autofill = jest.fn().mockResolvedValue({});
|
|
84
77
|
|
|
85
|
-
const
|
|
86
|
-
expect(
|
|
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
|
-
});
|
|
78
|
+
const result = await manager.deposit(100);
|
|
79
|
+
expect(result).toBe("SUCCESS");
|
|
94
80
|
});
|
|
95
81
|
|
|
96
|
-
test("throws ChannelNotFound
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await expect(manager.
|
|
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, "XRPL.MAINNET", currency, mockHttp);
|
|
86
|
+
await expect(manager.getAuthToken(100)).rejects.toThrow(ChannelNotFound);
|
|
87
|
+
await expect(manager.getAuthToken(100)).rejects.toThrow(/No open payment channel from/);
|
|
88
|
+
expect(configUtils.retrieveChannelIdFromFirestoreRest).toHaveBeenCalledWith(
|
|
89
|
+
"XRPL.MAINNET",
|
|
90
|
+
currency,
|
|
101
91
|
wallet.classicAddress,
|
|
92
|
+
mockHttp
|
|
102
93
|
);
|
|
103
|
-
await expect(manager._findChannel()).rejects.toThrow(manager.destination);
|
|
104
94
|
});
|
|
105
|
-
});
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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" },
|
|
96
|
+
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
|
+
},
|
|
117
102
|
});
|
|
118
103
|
|
|
119
|
-
|
|
120
|
-
expect(
|
|
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");
|
|
104
|
+
await expect(manager.getAuthToken(100)).rejects.toThrow(ChannelNotFound);
|
|
105
|
+
await expect(manager.getAuthToken(100)).rejects.toThrow(/FIRESTORE_ID not found on-chain/);
|
|
156
106
|
});
|
|
157
107
|
});
|
|
158
108
|
|
|
159
109
|
describe("getAuthToken", () => {
|
|
160
110
|
test("success with default amount", async () => {
|
|
161
|
-
// stub channel lookup
|
|
162
111
|
manager._findChannel = jest.fn().mockResolvedValue({
|
|
163
112
|
channel_id: CHANNEL_ID,
|
|
164
113
|
amount: "1001",
|
|
165
114
|
});
|
|
166
115
|
|
|
167
|
-
|
|
168
|
-
const claimSpy = jest
|
|
169
|
-
.spyOn(createSignedClaim, "buildPaychanAuthHexStringToBeSigned")
|
|
170
|
-
.mockReturnValue("CLAIMHEX");
|
|
171
|
-
|
|
172
|
-
// stub signature
|
|
173
|
-
mockRippleSign.mockReturnValue("SIGVALUE");
|
|
116
|
+
rippleKeypairs.sign.mockReturnValue("SIG");
|
|
174
117
|
|
|
175
118
|
const token = await manager.getAuthToken();
|
|
176
119
|
const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
|
|
177
120
|
|
|
178
121
|
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
122
|
channel_id: CHANNEL_ID,
|
|
186
|
-
|
|
123
|
+
authorized_to_claim: "1001",
|
|
124
|
+
signature: "SIG",
|
|
187
125
|
});
|
|
188
|
-
|
|
189
|
-
// ensure our spy was called with correct args
|
|
190
|
-
expect(claimSpy).toHaveBeenCalledWith(CHANNEL_ID, "1001");
|
|
191
126
|
});
|
|
192
127
|
|
|
193
128
|
test("success with specific amount", async () => {
|
|
@@ -195,16 +130,11 @@ describe("DhaliChannelManager", () => {
|
|
|
195
130
|
channel_id: CHANNEL_ID,
|
|
196
131
|
amount: "500",
|
|
197
132
|
});
|
|
198
|
-
|
|
199
|
-
const claimSpy = jest
|
|
200
|
-
.spyOn(createSignedClaim, "buildPaychanAuthHexStringToBeSigned")
|
|
201
|
-
.mockReturnValue("CLAIMHEX2");
|
|
202
|
-
mockRippleSign.mockReturnValue("SIG2");
|
|
133
|
+
rippleKeypairs.sign.mockReturnValue("SIG2");
|
|
203
134
|
|
|
204
135
|
const token = await manager.getAuthToken(200);
|
|
205
136
|
const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8"));
|
|
206
137
|
expect(decoded.authorized_to_claim).toBe("200");
|
|
207
|
-
expect(claimSpy).toHaveBeenCalledWith(CHANNEL_ID, "200");
|
|
208
138
|
});
|
|
209
139
|
|
|
210
140
|
test("throws if amount exceeds capacity", async () => {
|