@glowlabs-org/utils 0.1.5 → 0.2.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,564 @@
1
+ import { BigNumber, ethers } from "ethers";
2
+ import { FORWARDER_ABI } from "../abis/forwarderABI";
3
+ import { ERC20_ABI } from "../abis/erc20.abi";
4
+ import { getAddresses } from "../../constants/addresses";
5
+
6
+ export enum ForwarderError {
7
+ CONTRACT_NOT_AVAILABLE = "Contract not available",
8
+ SIGNER_NOT_AVAILABLE = "Signer not available",
9
+ UNKNOWN_ERROR = "Unknown error",
10
+ INVALID_FORWARD_TYPE = "Invalid forward type",
11
+ MISSING_REQUIRED_PARAMS = "Missing required parameters",
12
+ }
13
+
14
+ // Forward types based on API router documentation
15
+ export type ForwardType =
16
+ | "PayProtocolFeeAndMintGCTLAndStake"
17
+ | "PayProtocolFee"
18
+ | "MintGCTLAndStake"
19
+ | "MintGCTL"
20
+ | "BuySolarFarm";
21
+
22
+ // Currency types
23
+ export type Currency = "USDC" | "GLW" | "USDG";
24
+
25
+ // Forward parameters interface
26
+ export interface ForwardParams {
27
+ amount: BigNumber;
28
+ userAddress: string;
29
+ type: ForwardType;
30
+ currency?: Currency;
31
+ applicationId?: string;
32
+ farmId?: string;
33
+ regionId?: number;
34
+ }
35
+
36
+ // Utility to extract the most useful revert reason from an ethers error object
37
+ function parseEthersError(error: unknown): string {
38
+ if (!error) return "Unknown error";
39
+ const possibleError: any = error;
40
+
41
+ // If the error originates from a callStatic it will often be found at `error?.error?.body`
42
+ if (possibleError?.error?.body) {
43
+ try {
44
+ const body = JSON.parse(possibleError.error.body);
45
+ // Hardhat style errors
46
+ if (body?.error?.message) return body.error.message as string;
47
+ } catch {}
48
+ }
49
+
50
+ // Found on MetaMask/Alchemy shape errors
51
+ if (possibleError?.data?.message) return possibleError.data.message as string;
52
+ if (possibleError?.error?.message)
53
+ return possibleError.error.message as string;
54
+
55
+ // Standard ethers v5 message
56
+ if (possibleError?.reason) return possibleError.reason as string;
57
+ if (possibleError?.message) return possibleError.message as string;
58
+
59
+ return ForwarderError.UNKNOWN_ERROR;
60
+ }
61
+
62
+ // Type-guard style helper to ensure a signer exists throughout the rest of the function.
63
+ function assertSigner(
64
+ maybeSigner: ethers.providers.JsonRpcSigner | undefined
65
+ ): asserts maybeSigner is ethers.providers.JsonRpcSigner {
66
+ if (!maybeSigner) {
67
+ throw new Error(ForwarderError.SIGNER_NOT_AVAILABLE);
68
+ }
69
+ }
70
+
71
+ export function useForwarder(
72
+ signer: ethers.providers.JsonRpcSigner | undefined,
73
+ CHAIN_ID: number
74
+ ) {
75
+ // Use dynamic addresses based on chain configuration
76
+ const ADDRESSES = getAddresses(CHAIN_ID);
77
+
78
+ // Framework-agnostic processing flag
79
+ let isProcessing = false;
80
+ const setIsProcessing = (value: boolean) => {
81
+ isProcessing = value;
82
+ };
83
+
84
+ // Returns a contract instance for Forwarder
85
+ function getForwarderContract() {
86
+ assertSigner(signer);
87
+ return new ethers.Contract(ADDRESSES.FORWARDER, FORWARDER_ABI, signer);
88
+ }
89
+
90
+ /**
91
+ * Construct the message for the forward call based on type and parameters
92
+ */
93
+ function constructForwardMessage(params: ForwardParams): string {
94
+ const { type, applicationId, farmId, regionId, userAddress } = params;
95
+
96
+ switch (type) {
97
+ case "PayProtocolFeeAndMintGCTLAndStake":
98
+ if (!applicationId) {
99
+ throw new Error(ForwarderError.MISSING_REQUIRED_PARAMS);
100
+ }
101
+ return `PayProtocolFeeAndMintGCTLAndStake::${applicationId}`;
102
+
103
+ case "PayProtocolFee":
104
+ if (!applicationId) {
105
+ throw new Error(ForwarderError.MISSING_REQUIRED_PARAMS);
106
+ }
107
+ return `PayProtocolFee::${applicationId}`;
108
+
109
+ case "MintGCTLAndStake":
110
+ if (!regionId) {
111
+ throw new Error(ForwarderError.MISSING_REQUIRED_PARAMS);
112
+ }
113
+ return `MintGCTLAndStake::${regionId}`;
114
+
115
+ case "MintGCTL":
116
+ if (!userAddress) {
117
+ throw new Error(ForwarderError.MISSING_REQUIRED_PARAMS);
118
+ }
119
+ return `MintGCTL::${userAddress}`;
120
+
121
+ case "BuySolarFarm":
122
+ if (!farmId) {
123
+ throw new Error(ForwarderError.MISSING_REQUIRED_PARAMS);
124
+ }
125
+ return `BuySolarFarm::${farmId}`;
126
+
127
+ default:
128
+ throw new Error(ForwarderError.INVALID_FORWARD_TYPE);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get the appropriate token contract based on currency
134
+ */
135
+ function getTokenContract(currency: Currency = "USDC") {
136
+ assertSigner(signer);
137
+
138
+ let tokenAddress: string;
139
+ switch (currency) {
140
+ case "USDC":
141
+ tokenAddress = ADDRESSES.USDC;
142
+ break;
143
+ case "GLW":
144
+ tokenAddress = ADDRESSES.GLW;
145
+ break;
146
+ case "USDG":
147
+ tokenAddress = ADDRESSES.USDG;
148
+ break;
149
+ default:
150
+ throw new Error(
151
+ `Currency ${currency} not yet supported. Only USDC, GLW, and USDG are currently supported.`
152
+ );
153
+ }
154
+
155
+ return new ethers.Contract(tokenAddress, ERC20_ABI, signer);
156
+ }
157
+
158
+ /**
159
+ * Check current token allowance for the forwarder contract
160
+ * @param owner The wallet address to check allowance for
161
+ * @param currency The currency to check allowance for
162
+ */
163
+ async function checkTokenAllowance(
164
+ owner: string,
165
+ currency: Currency = "USDC"
166
+ ): Promise<BigNumber> {
167
+ assertSigner(signer);
168
+
169
+ try {
170
+ const tokenContract = getTokenContract(currency);
171
+ if (!tokenContract)
172
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
173
+
174
+ const allowance: BigNumber = await tokenContract.allowance(
175
+ owner,
176
+ ADDRESSES.FORWARDER
177
+ );
178
+ return allowance;
179
+ } catch (error) {
180
+ throw new Error(parseEthersError(error));
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Check user's token balance
186
+ * @param owner The wallet address to check balance for
187
+ * @param currency The currency to check balance for
188
+ */
189
+ async function checkTokenBalance(
190
+ owner: string,
191
+ currency: Currency = "USDC"
192
+ ): Promise<BigNumber> {
193
+ assertSigner(signer);
194
+
195
+ try {
196
+ const tokenContract = getTokenContract(currency);
197
+ if (!tokenContract)
198
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
199
+
200
+ const balance: BigNumber = await tokenContract.balanceOf(owner);
201
+ return balance;
202
+ } catch (error) {
203
+ throw new Error(parseEthersError(error));
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Approve tokens for the forwarder contract
209
+ * @param amount Amount to approve (BigNumber)
210
+ * @param currency The currency to approve
211
+ */
212
+ async function approveToken(
213
+ amount: BigNumber,
214
+ currency: Currency = "USDC"
215
+ ): Promise<boolean> {
216
+ assertSigner(signer);
217
+
218
+ try {
219
+ const tokenContract = getTokenContract(currency);
220
+ if (!tokenContract)
221
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
222
+
223
+ setIsProcessing(true);
224
+
225
+ // Approve only the specific amount needed
226
+ const approveTx = await tokenContract.approve(
227
+ ADDRESSES.FORWARDER,
228
+ amount
229
+ );
230
+ await approveTx.wait();
231
+
232
+ return true;
233
+ } catch (error) {
234
+ throw new Error(parseEthersError(error));
235
+ } finally {
236
+ setIsProcessing(false);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Forward tokens through the forwarder contract with type-specific handling
242
+ * @param params Forward parameters including type, amount, and required fields
243
+ */
244
+ async function forwardTokens(params: ForwardParams): Promise<string> {
245
+ assertSigner(signer);
246
+
247
+ try {
248
+ const forwarderContract = getForwarderContract();
249
+ if (!forwarderContract)
250
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
251
+
252
+ setIsProcessing(true);
253
+
254
+ const { amount, currency = "USDC" } = params;
255
+ const tokenContract = getTokenContract(currency);
256
+ if (!tokenContract)
257
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
258
+
259
+ const owner = await signer.getAddress();
260
+
261
+ // Construct the appropriate message for this forward type
262
+ const message = constructForwardMessage(params);
263
+
264
+ // Check allowance and approve if necessary
265
+ const allowance: BigNumber = await tokenContract.allowance(
266
+ owner,
267
+ ADDRESSES.FORWARDER
268
+ );
269
+
270
+ if (allowance.lt(amount)) {
271
+ try {
272
+ const approveTx = await tokenContract.approve(
273
+ ADDRESSES.FORWARDER,
274
+ ethers.constants.MaxUint256
275
+ );
276
+ await approveTx.wait();
277
+ } catch (approveError) {
278
+ throw new Error(
279
+ parseEthersError(approveError) || "Token approval failed"
280
+ );
281
+ }
282
+ }
283
+
284
+ // Get the token address based on currency
285
+ let tokenAddress: string;
286
+ switch (currency) {
287
+ case "USDC":
288
+ tokenAddress = ADDRESSES.USDC;
289
+ break;
290
+ case "USDG":
291
+ tokenAddress = ADDRESSES.USDG;
292
+ break;
293
+ case "GLW":
294
+ tokenAddress = ADDRESSES.GLW;
295
+ break;
296
+ default:
297
+ throw new Error(`Unsupported currency for forwarding: ${currency}`);
298
+ }
299
+
300
+ // Run a static call first to surface any revert reason
301
+ try {
302
+ await forwarderContract.callStatic.forward(
303
+ tokenAddress,
304
+ ADDRESSES.FOUNDATION_WALLET,
305
+ amount,
306
+ message,
307
+ { from: owner }
308
+ );
309
+ } catch (staticError) {
310
+ throw new Error(parseEthersError(staticError));
311
+ }
312
+
313
+ // Execute the forward transaction
314
+ const tx = await forwarderContract.forward(
315
+ tokenAddress,
316
+ ADDRESSES.FOUNDATION_WALLET,
317
+ amount,
318
+ message
319
+ );
320
+ await tx.wait();
321
+
322
+ return tx.hash;
323
+ } catch (txError: any) {
324
+ throw new Error(parseEthersError(txError));
325
+ } finally {
326
+ setIsProcessing(false);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Forward tokens for protocol fee payment and GCTL minting with staking
332
+ */
333
+ async function payProtocolFeeAndMintGCTLAndStake(
334
+ amount: BigNumber,
335
+ userAddress: string,
336
+ applicationId: string,
337
+ regionId?: number,
338
+ currency: Currency = "USDC"
339
+ ): Promise<string> {
340
+ assertSigner(signer);
341
+
342
+ // GCTL minting only supports USDC and USDG
343
+ if (currency === "GLW") {
344
+ throw new Error(
345
+ "GCTL minting is not supported with GLW payment. Use USDC or USDG."
346
+ );
347
+ }
348
+
349
+ return forwardTokens({
350
+ amount,
351
+ userAddress,
352
+ type: "PayProtocolFeeAndMintGCTLAndStake",
353
+ currency,
354
+ applicationId,
355
+ regionId,
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Forward tokens for protocol fee payment only
361
+ */
362
+ async function payProtocolFee(
363
+ amount: BigNumber,
364
+ userAddress: string,
365
+ applicationId: string,
366
+ currency: Currency = "USDC"
367
+ ): Promise<string> {
368
+ assertSigner(signer);
369
+
370
+ return forwardTokens({
371
+ amount,
372
+ userAddress,
373
+ type: "PayProtocolFee",
374
+ currency,
375
+ applicationId,
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Forward USDC to mint GCTL and stake to a region
381
+ */
382
+ async function mintGCTLAndStake(
383
+ amount: BigNumber,
384
+ userAddress: string,
385
+ regionId?: number
386
+ ): Promise<string> {
387
+ assertSigner(signer);
388
+
389
+ return forwardTokens({
390
+ amount,
391
+ userAddress,
392
+ type: "MintGCTLAndStake",
393
+ currency: "USDC",
394
+ regionId,
395
+ });
396
+ }
397
+
398
+ /**
399
+ * Forward USDC to mint GCTL (existing functionality, keeping for compatibility)
400
+ */
401
+ async function mintGCTL(
402
+ amount: BigNumber,
403
+ userAddress: string
404
+ ): Promise<string> {
405
+ assertSigner(signer);
406
+
407
+ return forwardTokens({
408
+ amount,
409
+ userAddress,
410
+ type: "MintGCTL",
411
+ currency: "USDC",
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Forward tokens to buy a solar farm
417
+ */
418
+ async function buySolarFarm(
419
+ amount: BigNumber,
420
+ userAddress: string,
421
+ farmId: string,
422
+ currency: Currency = "USDC"
423
+ ): Promise<string> {
424
+ assertSigner(signer);
425
+
426
+ return forwardTokens({
427
+ amount,
428
+ userAddress,
429
+ type: "BuySolarFarm",
430
+ currency,
431
+ farmId,
432
+ });
433
+ }
434
+
435
+ /**
436
+ * Estimate gas for forwarding with type-specific handling
437
+ * @param params Forward parameters
438
+ * @param ethPriceInUSD Current ETH price in USD (for cost estimation)
439
+ */
440
+ async function estimateGasForForward(
441
+ params: ForwardParams,
442
+ ethPriceInUSD: number | null
443
+ ): Promise<string> {
444
+ assertSigner(signer);
445
+
446
+ try {
447
+ const forwarderContract = getForwarderContract();
448
+ if (!forwarderContract)
449
+ throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
450
+
451
+ const { amount, currency = "USDC" } = params;
452
+
453
+ // Construct the appropriate message for this forward type
454
+ const message = constructForwardMessage(params);
455
+
456
+ // Get token address
457
+ let tokenAddress: string;
458
+ switch (currency) {
459
+ case "USDC":
460
+ tokenAddress = ADDRESSES.USDC;
461
+ break;
462
+ case "USDG":
463
+ tokenAddress = ADDRESSES.USDG;
464
+ break;
465
+ case "GLW":
466
+ tokenAddress = ADDRESSES.GLW;
467
+ break;
468
+ default:
469
+ throw new Error(
470
+ `Unsupported currency for gas estimation: ${currency}`
471
+ );
472
+ }
473
+
474
+ const gasPrice = await signer.getGasPrice();
475
+ const estimatedGas = await forwarderContract.estimateGas.forward(
476
+ tokenAddress,
477
+ ADDRESSES.FOUNDATION_WALLET,
478
+ amount,
479
+ message
480
+ );
481
+ const estimatedCost = estimatedGas.mul(gasPrice);
482
+
483
+ if (ethPriceInUSD) {
484
+ const estimatedCostInEth = ethers.utils.formatEther(estimatedCost);
485
+ const estimatedCostInUSD = (
486
+ parseFloat(estimatedCostInEth) * ethPriceInUSD
487
+ ).toFixed(2);
488
+ return estimatedCostInUSD;
489
+ } else {
490
+ throw new Error(
491
+ "Could not fetch the ETH price to calculate cost in USD."
492
+ );
493
+ }
494
+ } catch (error: any) {
495
+ throw new Error(parseEthersError(error));
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Mint test USDC (only works on testnets with mintable USDC contracts)
501
+ * @param amount Amount of USDC to mint (BigNumber, 6 decimals)
502
+ * @param recipient Address to mint USDC to
503
+ */
504
+ async function mintTestUSDC(
505
+ amount: BigNumber,
506
+ recipient: string
507
+ ): Promise<string> {
508
+ assertSigner(signer);
509
+ if (CHAIN_ID !== 11155111) {
510
+ throw new Error("Minting test USDC is only supported on Sepolia");
511
+ }
512
+
513
+ try {
514
+ const usdcContract = getTokenContract("USDC"); // Use getTokenContract for consistency
515
+ if (!usdcContract) throw new Error(ForwarderError.CONTRACT_NOT_AVAILABLE);
516
+
517
+ setIsProcessing(true);
518
+
519
+ // Try to call mint function (common for test tokens)
520
+ const tx = await usdcContract.mint(recipient, amount);
521
+ await tx.wait();
522
+
523
+ return tx.hash;
524
+ } catch (error: any) {
525
+ // If mint function doesn't exist or fails, provide helpful error
526
+ const errorMessage = parseEthersError(error);
527
+ if (errorMessage.includes("mint")) {
528
+ throw new Error("This USDC contract doesn't support minting");
529
+ }
530
+ throw new Error(errorMessage);
531
+ } finally {
532
+ setIsProcessing(false);
533
+ }
534
+ }
535
+
536
+ return {
537
+ // New methods for different forward types
538
+ forwardTokens,
539
+ payProtocolFeeAndMintGCTLAndStake,
540
+ payProtocolFee,
541
+ mintGCTLAndStake,
542
+ mintGCTL,
543
+ buySolarFarm,
544
+
545
+ // Token operations
546
+ approveToken,
547
+ checkTokenAllowance,
548
+ checkTokenBalance,
549
+
550
+ // Utility methods
551
+ estimateGasForForward,
552
+ mintTestUSDC,
553
+ constructForwardMessage,
554
+
555
+ // State
556
+ get isProcessing() {
557
+ return isProcessing;
558
+ },
559
+ addresses: ADDRESSES,
560
+
561
+ // Signer availability
562
+ isSignerAvailable: !!signer,
563
+ };
564
+ }