dhali-js 2.1.0 → 3.0.3
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 +1 -1
- package/README.md +120 -61
- package/package.json +2 -2
- package/src/dhali/Currency.js +2 -1
- package/src/dhali/DhaliChannelManager.js +7 -10
- package/src/dhali/DhaliEthChannelManager.js +314 -300
- package/src/dhali/DhaliXrplChannelManager.js +6 -8
- package/src/dhali/configUtils.js +5 -13
- package/src/dhali/utils.js +8 -0
- package/src/index.js +1 -1
- package/tests/DhaliChannelManager.test.js +25 -3
- package/tests/DhaliEthChannelManager.test.js +116 -50
|
@@ -1,331 +1,345 @@
|
|
|
1
1
|
const { getEthereumClaimTypedData } = require("./createSignedClaim");
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
keccak256,
|
|
4
|
+
encodeAbiParameters,
|
|
5
|
+
parseAbiParameters,
|
|
6
|
+
toHex
|
|
7
|
+
} = require("viem");
|
|
8
|
+
const crypto = require("crypto");
|
|
3
9
|
|
|
4
10
|
const { fetchPublicConfig, notifyAdminGateway, retrieveChannelIdFromFirestoreRest } = require("./configUtils");
|
|
11
|
+
const { ChannelNotFound } = require("./utils");
|
|
5
12
|
|
|
6
13
|
class DhaliEthChannelManager {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {import("viem").WalletClient} walletClient
|
|
16
|
+
* @param {import("viem").PublicClient} publicClient
|
|
17
|
+
* @param {import("./Currency")} currency
|
|
18
|
+
* @param {typeof fetch} [httpClient] - Injected HTTP client
|
|
19
|
+
* @param {object} [public_config] - Dhali public configuration
|
|
20
|
+
*/
|
|
21
|
+
constructor(walletClient, publicClient, currency, httpClient = fetch, public_config) {
|
|
22
|
+
this.walletClient = walletClient;
|
|
23
|
+
this.publicClient = publicClient;
|
|
24
|
+
this.currency = currency;
|
|
25
|
+
this.httpClient = httpClient || fetch;
|
|
26
|
+
this.public_config = public_config;
|
|
27
|
+
this.chainId = this._getChainIdFromProtocol(this.currency.network);
|
|
28
|
+
this.destinationAddress = undefined;
|
|
29
|
+
this.contractAddress = undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_getChainIdFromProtocol(protocol) {
|
|
33
|
+
switch (protocol.toUpperCase()) {
|
|
34
|
+
case "ETHEREUM": return 1;
|
|
35
|
+
case "SEPOLIA": return 11155111;
|
|
36
|
+
case "HOLESKY": return 17000;
|
|
37
|
+
case "HARDHAT": return 31337;
|
|
38
|
+
default: throw new Error(`Unsupported protocol: ${protocol}`);
|
|
25
39
|
}
|
|
40
|
+
}
|
|
26
41
|
|
|
27
|
-
_getChainIdFromProtocol(protocol) {
|
|
28
|
-
switch (protocol) {
|
|
29
|
-
case "ETHEREUM": return 1;
|
|
30
|
-
case "SEPOLIA": return 11155111;
|
|
31
|
-
case "HOLESKY": return 17000;
|
|
32
|
-
case "LOCALHOST": return 31337;
|
|
33
|
-
default: throw new Error(`Unsupported protocol: ${protocol}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
43
|
+
async _resolveAddresses() {
|
|
44
|
+
if (this.destinationAddress && this.contractAddress) return;
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!this.public_config) {
|
|
45
|
-
this.public_config = await fetchPublicConfig(this.httpClient);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!this.destinationAddress) {
|
|
49
|
-
try {
|
|
50
|
-
this.destinationAddress = this.public_config.DHALI_PUBLIC_ADDRESSES[this.protocol][this.currency.code].wallet_id;
|
|
51
|
-
} catch (e) {
|
|
52
|
-
throw new Error("Destination address not found in public_config for this protocol/currency: " + e.message);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!this.contractAddress) {
|
|
57
|
-
try {
|
|
58
|
-
// @ts-ignore
|
|
59
|
-
this.contractAddress = this.public_config.CONTRACTS[this.protocol].contract_address;
|
|
60
|
-
} catch (e) {
|
|
61
|
-
throw new Error("Contract address not found in public_config for this protocol: " + e.message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!this.contractAddress) {
|
|
66
|
-
throw new Error("Contract address must be provided or resolved for this chainId");
|
|
67
|
-
}
|
|
46
|
+
if (!this.public_config) {
|
|
47
|
+
this.public_config = await fetchPublicConfig(this.httpClient);
|
|
68
48
|
}
|
|
69
49
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const address = (await this.signer.getAddress()).toLowerCase();
|
|
77
|
-
return await retrieveChannelIdFromFirestoreRest(
|
|
78
|
-
this.protocol,
|
|
79
|
-
this.currency,
|
|
80
|
-
address,
|
|
81
|
-
this.httpClient
|
|
82
|
-
);
|
|
50
|
+
if (!this.destinationAddress) {
|
|
51
|
+
try {
|
|
52
|
+
this.destinationAddress = this.public_config.DHALI_PUBLIC_ADDRESSES[this.currency.network][this.currency.code].wallet_id.toLowerCase();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new Error("Destination address not found in public_config for this protocol/currency: " + e.message);
|
|
55
|
+
}
|
|
83
56
|
}
|
|
84
57
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return null;
|
|
58
|
+
if (!this.contractAddress) {
|
|
59
|
+
try {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
this.contractAddress = this.public_config.CONTRACTS[this.currency.network].contract_address;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error("Contract address not found in public_config for this protocol: " + e.message);
|
|
64
|
+
}
|
|
93
65
|
}
|
|
94
66
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const sender = await this.signer.getAddress();
|
|
98
|
-
return ethers.keccak256(
|
|
99
|
-
ethers.AbiCoder.defaultAbiCoder().encode(
|
|
100
|
-
["address", "address", "address", "uint256"],
|
|
101
|
-
[sender, receiver, tokenAddress, nonce]
|
|
102
|
-
)
|
|
103
|
-
);
|
|
67
|
+
if (!this.contractAddress) {
|
|
68
|
+
throw new Error("Contract address must be provided or resolved for this chainId");
|
|
104
69
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Queries Firestore for an existing open channel.
|
|
74
|
+
* Path: public_claim_info/<protocol>/<currency_identifier>
|
|
75
|
+
* Filter: account == my_address, closed != true
|
|
76
|
+
*/
|
|
77
|
+
async _retrieveChannelIdFromFirestore() {
|
|
78
|
+
const [address] = await this.walletClient.getAddresses();
|
|
79
|
+
return await retrieveChannelIdFromFirestoreRest(
|
|
80
|
+
this.currency.network,
|
|
81
|
+
this.currency,
|
|
82
|
+
address.toLowerCase(),
|
|
83
|
+
this.httpClient
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async _retrieveChannelIdFromFirestoreWithPolling(timeoutSeconds = 30) {
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
while (Date.now() - startTime < timeoutSeconds * 1000) {
|
|
90
|
+
const channelId = await this._retrieveChannelIdFromFirestore();
|
|
91
|
+
if (channelId) return channelId;
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
108
93
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async _calculateChannelId(receiver, tokenAddress, nonce) {
|
|
98
|
+
// Matches Dhali-wallet: keccak256(abi.encode(sender, receiver, token, nonce))
|
|
99
|
+
const [sender] = await this.walletClient.getAddresses();
|
|
100
|
+
return keccak256(
|
|
101
|
+
encodeAbiParameters(
|
|
102
|
+
parseAbiParameters("address, address, address, uint256"),
|
|
103
|
+
[sender, receiver, tokenAddress, nonce]
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_encodeAddress(address) {
|
|
109
|
+
return address.toLowerCase().replace("0x", "").padStart(64, '0');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_encodeUint(value) {
|
|
113
|
+
return BigInt(value).toString(16).padStart(64, '0');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_encodeBool(value) {
|
|
117
|
+
return value ? "1".padStart(64, '0') : "0".padStart(64, '0');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_encodeBytes32(value) {
|
|
121
|
+
return value.replace("0x", "").padStart(64, '0');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async _buildTx(to, data, value) {
|
|
125
|
+
const accountString = (await this.walletClient.getAddresses())[0];
|
|
126
|
+
const account = this.walletClient.account || accountString;
|
|
127
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
128
|
+
// Add 10% buffer to gas price
|
|
129
|
+
const gasPriceWithBuffer = (gasPrice * BigInt(110)) / BigInt(100);
|
|
130
|
+
|
|
131
|
+
/** @type {any} */
|
|
132
|
+
const txParams = {
|
|
133
|
+
account: account,
|
|
134
|
+
to: to,
|
|
135
|
+
value: value,
|
|
136
|
+
data: data,
|
|
137
|
+
gasPrice: gasPriceWithBuffer,
|
|
138
|
+
nonce: await this.publicClient.getTransactionCount({ address: accountString, blockTag: "pending" }),
|
|
139
|
+
chain: { id: this.chainId }
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Estimate gas
|
|
143
|
+
const gasLimit = await this.publicClient.estimateGas(txParams);
|
|
144
|
+
// Add 10% buffer to gas limit
|
|
145
|
+
txParams.gas = (gasLimit * BigInt(110)) / BigInt(100);
|
|
146
|
+
|
|
147
|
+
return txParams;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Deposits funds into a payment channel.
|
|
152
|
+
* If an open channel exists, funds it.
|
|
153
|
+
* If not, opens a new one.
|
|
154
|
+
* @param {string|number} amount Amount in base units (wei/drops)
|
|
155
|
+
* @returns {Promise<import("viem").TransactionReceipt>}
|
|
156
|
+
*/
|
|
157
|
+
async deposit(amount) {
|
|
158
|
+
await this._resolveAddresses();
|
|
159
|
+
const existingChannelId = await this._retrieveChannelIdFromFirestore();
|
|
160
|
+
const tokenAddress = this.currency.tokenAddress || "0x0000000000000000000000000000000000000000";
|
|
161
|
+
const isNative = (tokenAddress === "0x0000000000000000000000000000000000000000");
|
|
162
|
+
const amountBig = BigInt(amount);
|
|
163
|
+
|
|
164
|
+
const OPEN_CHANNEL_SELECTOR = "3cd880a5";
|
|
165
|
+
const DEPOSIT_SELECTOR = "264d06c8";
|
|
166
|
+
const SETTLE_DELAY = 1209600n; // 2 weeks
|
|
167
|
+
|
|
168
|
+
if (existingChannelId) {
|
|
169
|
+
// Deposit
|
|
170
|
+
const calldata = "0x" +
|
|
171
|
+
DEPOSIT_SELECTOR +
|
|
172
|
+
this._encodeBytes32(existingChannelId) +
|
|
173
|
+
this._encodeUint(amountBig) +
|
|
174
|
+
this._encodeBool(true); // renew
|
|
175
|
+
|
|
176
|
+
if (!isNative) {
|
|
177
|
+
await this._approveToken(tokenAddress, this.contractAddress, amountBig);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const txParams = await this._buildTx(this.contractAddress, calldata, isNative ? amountBig : 0n);
|
|
181
|
+
const hash = await this.walletClient.sendTransaction(txParams);
|
|
182
|
+
return await this.publicClient.waitForTransactionReceipt({ hash });
|
|
183
|
+
|
|
184
|
+
} else {
|
|
185
|
+
// Open Channel
|
|
186
|
+
const receiver = this.destinationAddress;
|
|
187
|
+
const nonce = this._generateNonce();
|
|
188
|
+
const dummySigner = "0x0000000000000000000000000000000000000000";
|
|
189
|
+
|
|
190
|
+
const calldata = "0x" +
|
|
191
|
+
OPEN_CHANNEL_SELECTOR +
|
|
192
|
+
this._encodeAddress(receiver) +
|
|
193
|
+
this._encodeAddress(tokenAddress) +
|
|
194
|
+
this._encodeUint(amountBig) +
|
|
195
|
+
this._encodeUint(SETTLE_DELAY) +
|
|
196
|
+
this._encodeUint(nonce) +
|
|
197
|
+
this._encodeAddress(dummySigner);
|
|
198
|
+
|
|
199
|
+
if (!isNative) {
|
|
200
|
+
await this._approveToken(tokenAddress, this.contractAddress, amountBig);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const txParams = await this._buildTx(this.contractAddress, calldata, isNative ? amountBig : 0n);
|
|
204
|
+
const hash = await this.walletClient.sendTransaction(txParams);
|
|
205
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({ hash });
|
|
206
|
+
|
|
207
|
+
// Calculate channel ID and notify gateway
|
|
208
|
+
const calculatedChannelId = await this._calculateChannelId(receiver, tokenAddress, nonce);
|
|
209
|
+
|
|
210
|
+
let currencyIdentifier = this.currency.code;
|
|
211
|
+
if (this.currency.tokenAddress) {
|
|
212
|
+
currencyIdentifier = `${this.currency.code}.${this.currency.tokenAddress}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const [address] = await this.walletClient.getAddresses();
|
|
216
|
+
// Proactive notification
|
|
217
|
+
await notifyAdminGateway(
|
|
218
|
+
this.currency.network,
|
|
219
|
+
currencyIdentifier,
|
|
220
|
+
address.toLowerCase(),
|
|
221
|
+
calculatedChannelId,
|
|
222
|
+
this.httpClient
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Poll Firestore to match setupBalanceListener behavior
|
|
226
|
+
await this._retrieveChannelIdFromFirestoreWithPolling(30);
|
|
227
|
+
|
|
228
|
+
return receipt;
|
|
112
229
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_generateNonce() {
|
|
233
|
+
const bytes = crypto.randomBytes(32);
|
|
234
|
+
return BigInt(toHex(bytes));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _approveToken(tokenAddress, spender, amount) {
|
|
238
|
+
const APPROVE_SELECTOR = "095ea7b3";
|
|
239
|
+
const calldata = "0x" +
|
|
240
|
+
APPROVE_SELECTOR +
|
|
241
|
+
this._encodeAddress(spender) +
|
|
242
|
+
this._encodeUint(amount);
|
|
243
|
+
|
|
244
|
+
const txParams = await this._buildTx(tokenAddress, calldata, 0n);
|
|
245
|
+
const hash = await this.walletClient.sendTransaction(txParams);
|
|
246
|
+
await this.publicClient.waitForTransactionReceipt({ hash });
|
|
247
|
+
}
|
|
248
|
+
async _getOnChainChannelAmount(channelId) {
|
|
249
|
+
const cleanId = channelId.replace("0x", "").padStart(64, "0");
|
|
250
|
+
const calldata = "0x831c2b82" + cleanId;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result = await this.publicClient.call({
|
|
254
|
+
to: this.contractAddress,
|
|
255
|
+
data: calldata
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (!result || result.data === "0x" || result.data.length < 322) {
|
|
259
|
+
throw new Error("Invalid getChannel response length");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The amount is the 5th 32-byte word (index 4).
|
|
263
|
+
// Result is a hex string "0x...".
|
|
264
|
+
// Word 0: 2 to 66
|
|
265
|
+
// Word 1: 66 to 130
|
|
266
|
+
// Word 2: 130 to 194
|
|
267
|
+
// Word 3: 194 to 258
|
|
268
|
+
// Word 4: 258 to 322
|
|
269
|
+
const amountHex = "0x" + result.data.substring(258, 322);
|
|
270
|
+
return BigInt(amountHex).toString();
|
|
271
|
+
} catch (e) {
|
|
272
|
+
throw new Error(`Failed to retrieve on-chain channel amount: ${e.message}`);
|
|
116
273
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Generate a base64-encoded payment claim.
|
|
278
|
+
* @param {number|string|null} amount - Defaults to total channel capacity if null
|
|
279
|
+
* @returns {Promise<string>}
|
|
280
|
+
*/
|
|
281
|
+
async getAuthToken(amount = null) {
|
|
282
|
+
await this._resolveAddresses();
|
|
283
|
+
// Poll Firestore if not found (setupBalanceListener simulation)
|
|
284
|
+
const channelIdRaw = await this._retrieveChannelIdFromFirestoreWithPolling(10);
|
|
285
|
+
if (!channelIdRaw) {
|
|
286
|
+
throw new ChannelNotFound("No open payment channel found in Firestore. Please deposit first.");
|
|
120
287
|
}
|
|
121
288
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const gasPrice = (feeData.gasPrice * BigInt(110)) / BigInt(100);
|
|
126
|
-
|
|
127
|
-
const txParams = {
|
|
128
|
-
from: await this.signer.getAddress(),
|
|
129
|
-
to: to,
|
|
130
|
-
value: value,
|
|
131
|
-
data: data,
|
|
132
|
-
gasPrice: gasPrice,
|
|
133
|
-
// Use pending nonce
|
|
134
|
-
nonce: await this.signer.getNonce("pending"),
|
|
135
|
-
chainId: this.chainId
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
// Estimate gas
|
|
139
|
-
const gasLimit = await this.signer.estimateGas(txParams);
|
|
140
|
-
// Add 10% buffer to gas limit
|
|
141
|
-
txParams.gasLimit = (gasLimit * BigInt(110)) / BigInt(100);
|
|
142
|
-
|
|
143
|
-
return txParams;
|
|
289
|
+
let channelId = channelIdRaw;
|
|
290
|
+
if (!channelId.startsWith("0x")) {
|
|
291
|
+
channelId = "0x" + channelId;
|
|
144
292
|
}
|
|
145
293
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* If an open channel exists, funds it.
|
|
149
|
-
* If not, opens a new one.
|
|
150
|
-
* @param {string|number} amount Amount in base units (wei/drops)
|
|
151
|
-
* @returns {Promise<ethers.TransactionReceipt>}
|
|
152
|
-
*/
|
|
153
|
-
async deposit(amount) {
|
|
154
|
-
await this._resolveAddresses();
|
|
155
|
-
const existingChannelId = await this._retrieveChannelIdFromFirestore();
|
|
156
|
-
const tokenAddress = this.currency.tokenAddress || "0x0000000000000000000000000000000000000000";
|
|
157
|
-
const isNative = (tokenAddress === "0x0000000000000000000000000000000000000000");
|
|
158
|
-
const amountBig = BigInt(amount);
|
|
159
|
-
|
|
160
|
-
const OPEN_CHANNEL_SELECTOR = "3cd880a5";
|
|
161
|
-
const DEPOSIT_SELECTOR = "264d06c8";
|
|
162
|
-
const SETTLE_DELAY = 1209600; // 2 weeks
|
|
163
|
-
|
|
164
|
-
if (existingChannelId) {
|
|
165
|
-
// Deposit
|
|
166
|
-
const calldata = "0x" +
|
|
167
|
-
DEPOSIT_SELECTOR +
|
|
168
|
-
this._encodeBytes32(existingChannelId) +
|
|
169
|
-
this._encodeUint(amountBig) +
|
|
170
|
-
this._encodeBool(true); // renew
|
|
171
|
-
|
|
172
|
-
if (!isNative) {
|
|
173
|
-
await this._approveToken(tokenAddress, this.contractAddress, amountBig);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const txParams = await this._buildTx(this.contractAddress, calldata, isNative ? amountBig : 0);
|
|
177
|
-
const tx = await this.signer.sendTransaction(txParams);
|
|
178
|
-
return await tx.wait();
|
|
179
|
-
|
|
180
|
-
} else {
|
|
181
|
-
// Open Channel
|
|
182
|
-
const receiver = this.destinationAddress;
|
|
183
|
-
const nonce = this._generateNonce();
|
|
184
|
-
const dummySigner = "0x0000000000000000000000000000000000000000";
|
|
185
|
-
|
|
186
|
-
const calldata = "0x" +
|
|
187
|
-
OPEN_CHANNEL_SELECTOR +
|
|
188
|
-
this._encodeAddress(receiver) +
|
|
189
|
-
this._encodeAddress(tokenAddress) +
|
|
190
|
-
this._encodeUint(amountBig) +
|
|
191
|
-
this._encodeUint(SETTLE_DELAY) +
|
|
192
|
-
this._encodeUint(nonce) +
|
|
193
|
-
this._encodeAddress(dummySigner);
|
|
194
|
-
|
|
195
|
-
if (!isNative) {
|
|
196
|
-
await this._approveToken(tokenAddress, this.contractAddress, amountBig);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const txParams = await this._buildTx(this.contractAddress, calldata, isNative ? amountBig : 0);
|
|
200
|
-
const tx = await this.signer.sendTransaction(txParams);
|
|
201
|
-
const receipt = await tx.wait();
|
|
202
|
-
|
|
203
|
-
// Calculate channel ID and notify gateway
|
|
204
|
-
const calculatedChannelId = await this._calculateChannelId(receiver, tokenAddress, nonce);
|
|
205
|
-
|
|
206
|
-
let currencyIdentifier = this.currency.code;
|
|
207
|
-
if (this.currency.tokenAddress) {
|
|
208
|
-
currencyIdentifier = `${this.currency.code}.${this.currency.tokenAddress}`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Proactive notification
|
|
212
|
-
await notifyAdminGateway(
|
|
213
|
-
this.protocol,
|
|
214
|
-
currencyIdentifier,
|
|
215
|
-
await this.signer.getAddress(),
|
|
216
|
-
calculatedChannelId,
|
|
217
|
-
this.httpClient
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Poll Firestore to match setupBalanceListener behavior
|
|
221
|
-
await this._retrieveChannelIdFromFirestoreWithPolling(30);
|
|
222
|
-
|
|
223
|
-
return receipt;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
294
|
+
const totalAmount = await this._getOnChainChannelAmount(channelId);
|
|
295
|
+
const allowed = amount !== null ? amount.toString() : totalAmount;
|
|
226
296
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
297
|
+
// BigInt comparison if needed, but for now simple check if it exceeds
|
|
298
|
+
if (BigInt(allowed) > BigInt(totalAmount)) {
|
|
299
|
+
throw new Error(`Requested auth ${allowed} exceeds channel capacity ${totalAmount}`);
|
|
230
300
|
}
|
|
231
301
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const calldata = "0x" +
|
|
235
|
-
APPROVE_SELECTOR +
|
|
236
|
-
this._encodeAddress(spender) +
|
|
237
|
-
this._encodeUint(amount);
|
|
238
|
-
|
|
239
|
-
const txParams = await this._buildTx(tokenAddress, calldata, 0);
|
|
240
|
-
const tx = await this.signer.sendTransaction(txParams);
|
|
241
|
-
await tx.wait();
|
|
242
|
-
}
|
|
243
|
-
async _getOnChainChannelAmount(channelId) {
|
|
244
|
-
const cleanId = channelId.replace("0x", "").padStart(64, "0");
|
|
245
|
-
const calldata = "0x831c2b82" + cleanId;
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const result = await this.rpc_client.call({
|
|
249
|
-
to: this.contractAddress,
|
|
250
|
-
data: calldata
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
if (result === "0x" || result.length < 322) {
|
|
254
|
-
throw new Error("Invalid getChannel response length");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// The amount is the 5th 32-byte word (index 4).
|
|
258
|
-
// Result is a hex string "0x...".
|
|
259
|
-
// Word 0: 2 to 66
|
|
260
|
-
// Word 1: 66 to 130
|
|
261
|
-
// Word 2: 130 to 194
|
|
262
|
-
// Word 3: 194 to 258
|
|
263
|
-
// Word 4: 258 to 322
|
|
264
|
-
const amountHex = "0x" + result.substring(258, 322);
|
|
265
|
-
return BigInt(amountHex).toString();
|
|
266
|
-
} catch (e) {
|
|
267
|
-
throw new Error(`Failed to retrieve on-chain channel amount: ${e.message}`);
|
|
268
|
-
}
|
|
302
|
+
if (!channelId.startsWith("0x")) {
|
|
303
|
+
channelId = "0x" + channelId;
|
|
269
304
|
}
|
|
270
305
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
this.chainId,
|
|
308
|
-
this.contractAddress
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const signature = await this.signer.signTypedData(domain, types, value);
|
|
312
|
-
|
|
313
|
-
const claim = {
|
|
314
|
-
version: "2",
|
|
315
|
-
account: await this.signer.getAddress(),
|
|
316
|
-
protocol: this.protocol,
|
|
317
|
-
currency: {
|
|
318
|
-
code: this.currency.code,
|
|
319
|
-
scale: this.currency.scale,
|
|
320
|
-
issuer: this.currency.tokenAddress || null
|
|
321
|
-
},
|
|
322
|
-
destination_account: this.destinationAddress,
|
|
323
|
-
authorized_to_claim: allowed,
|
|
324
|
-
channel_id: channelId,
|
|
325
|
-
signature: signature
|
|
326
|
-
};
|
|
327
|
-
return Buffer.from(JSON.stringify(claim)).toString("base64");
|
|
328
|
-
}
|
|
306
|
+
const token = this.currency.tokenAddress || "0x0000000000000000000000000000000000000000";
|
|
307
|
+
|
|
308
|
+
const { domain, types, value } = getEthereumClaimTypedData(
|
|
309
|
+
channelId,
|
|
310
|
+
token,
|
|
311
|
+
BigInt(allowed),
|
|
312
|
+
this.chainId,
|
|
313
|
+
this.contractAddress
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const accountString = (await this.walletClient.getAddresses())[0];
|
|
317
|
+
const account = this.walletClient.account || accountString;
|
|
318
|
+
const signature = await this.walletClient.signTypedData({
|
|
319
|
+
account,
|
|
320
|
+
domain,
|
|
321
|
+
types,
|
|
322
|
+
primaryType: 'DhaliClaim',
|
|
323
|
+
message: value
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const claim = {
|
|
327
|
+
version: "2",
|
|
328
|
+
account: accountString.toLowerCase(),
|
|
329
|
+
protocol: this.currency.network,
|
|
330
|
+
currency: {
|
|
331
|
+
code: this.currency.code,
|
|
332
|
+
scale: this.currency.scale,
|
|
333
|
+
issuer: this.currency.tokenAddress || null
|
|
334
|
+
},
|
|
335
|
+
destination_account: this.destinationAddress.toLowerCase(),
|
|
336
|
+
authorized_to_claim: allowed,
|
|
337
|
+
channel_id: channelId,
|
|
338
|
+
signature: signature
|
|
339
|
+
};
|
|
340
|
+
return Buffer.from(JSON.stringify(claim)).toString("base64");
|
|
341
|
+
}
|
|
329
342
|
}
|
|
330
343
|
|
|
331
344
|
module.exports = { DhaliEthChannelManager };
|
|
345
|
+
|