@obolnetwork/obol-sdk 2.3.0 → 2.4.1

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,70 @@
1
+ export const MerkleDistributorABI = {
2
+ abi: [
3
+ {
4
+ inputs: [
5
+ { internalType: 'address', name: 'token_', type: 'address' },
6
+ { internalType: 'bytes32', name: 'merkleRoot_', type: 'bytes32' },
7
+ ],
8
+ stateMutability: 'nonpayable',
9
+ type: 'constructor',
10
+ },
11
+ {
12
+ anonymous: false,
13
+ inputs: [
14
+ {
15
+ indexed: false,
16
+ internalType: 'uint256',
17
+ name: 'index',
18
+ type: 'uint256',
19
+ },
20
+ {
21
+ indexed: false,
22
+ internalType: 'address',
23
+ name: 'account',
24
+ type: 'address',
25
+ },
26
+ {
27
+ indexed: false,
28
+ internalType: 'uint256',
29
+ name: 'amount',
30
+ type: 'uint256',
31
+ },
32
+ ],
33
+ name: 'Claimed',
34
+ type: 'event',
35
+ },
36
+ {
37
+ inputs: [
38
+ { internalType: 'uint256', name: 'index', type: 'uint256' },
39
+ { internalType: 'address', name: 'account', type: 'address' },
40
+ { internalType: 'uint256', name: 'amount', type: 'uint256' },
41
+ { internalType: 'bytes32[]', name: 'merkleProof', type: 'bytes32[]' },
42
+ ],
43
+ name: 'claim',
44
+ outputs: [],
45
+ stateMutability: 'nonpayable',
46
+ type: 'function',
47
+ },
48
+ {
49
+ inputs: [{ internalType: 'uint256', name: 'index', type: 'uint256' }],
50
+ name: 'isClaimed',
51
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
52
+ stateMutability: 'view',
53
+ type: 'function',
54
+ },
55
+ {
56
+ inputs: [],
57
+ name: 'merkleRoot',
58
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
59
+ stateMutability: 'view',
60
+ type: 'function',
61
+ },
62
+ {
63
+ inputs: [],
64
+ name: 'token',
65
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
66
+ stateMutability: 'view',
67
+ type: 'function',
68
+ },
69
+ ] as const,
70
+ };
package/src/constants.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type TypedMessage } from '@metamask/eth-sig-util';
2
2
  import { type TypedDataDomain } from 'ethers';
3
- import * as pjson from '../package.json';
3
+ import pjson from '../package.json';
4
4
  import { FORK_MAPPING } from './types';
5
5
  import {
6
6
  HOLESKY_MULTICALL_BYTECODE,
@@ -0,0 +1,137 @@
1
+ import {
2
+ type JsonRpcApiProvider,
3
+ type JsonRpcProvider,
4
+ type JsonRpcSigner,
5
+ type Provider,
6
+ type Signer,
7
+ } from 'ethers';
8
+ import { isContractAvailable } from './utils';
9
+ import { type Incentives as IncentivesType, type ETH_ADDRESS } from './types';
10
+ import {
11
+ claimIncentivesFromMerkleDistributor,
12
+ isClaimedFromMerkleDistributor,
13
+ } from './incentivesHalpers';
14
+ import { DEFAULT_BASE_VERSION } from './constants';
15
+
16
+ export class Incentives {
17
+ private readonly signer: Signer | JsonRpcSigner | undefined;
18
+ public readonly chainId: number;
19
+ private readonly request: (
20
+ endpoint: string,
21
+ options?: RequestInit,
22
+ ) => Promise<any>;
23
+
24
+ public readonly provider:
25
+ | Provider
26
+ | JsonRpcProvider
27
+ | JsonRpcApiProvider
28
+ | undefined
29
+ | null;
30
+
31
+ constructor(
32
+ signer: Signer | JsonRpcSigner | undefined,
33
+ chainId: number,
34
+ request: (endpoint: string, options?: RequestInit) => Promise<any>,
35
+ provider:
36
+ | Provider
37
+ | JsonRpcProvider
38
+ | JsonRpcApiProvider
39
+ | undefined
40
+ | null,
41
+ ) {
42
+ this.signer = signer;
43
+ this.chainId = chainId;
44
+ this.request = request;
45
+ this.provider = provider;
46
+ }
47
+
48
+ /**
49
+ * Claims obol incentives from a Merkle Distributor contract.
50
+ *
51
+ * @remarks
52
+ * **⚠️ Important:** If you're storing the private key in an `.env` file, ensure it is securely managed
53
+ * and not pushed to version control.
54
+ *
55
+ * @param {Object} incentivesData - The incentives data needed for claiming.
56
+ * @param {string} incentivesData.contractAddress - The address of the Merkle Distributor contract.
57
+ * @param {number} incentivesData.index - The index in the Merkle tree.
58
+ * @param {string} incentivesData.operatorAddress - The address of the operator.
59
+ * @param {number} incentivesData.amount - The amount to claim.
60
+ * @param {string[]} incentivesData.merkleProof - The Merkle proof.
61
+ * @returns {Promise<{ txHash: string }>} The transaction hash of the claim transaction.
62
+ * @throws Will throw an error if the contract is not available or the claim fails.
63
+ *
64
+ */
65
+ async claimIncentives(incentivesData: {
66
+ contractAddress: ETH_ADDRESS;
67
+ index: number;
68
+ operatorAddress: ETH_ADDRESS;
69
+ amount: number;
70
+ merkleProof: string[];
71
+ }): Promise<{ txHash: string }> {
72
+ if (!this.signer) {
73
+ throw new Error('Signer is required in claimIncentives');
74
+ }
75
+ try {
76
+ const isContractDeployed = await isContractAvailable(
77
+ incentivesData.contractAddress,
78
+ this.signer.provider as Provider,
79
+ );
80
+
81
+ if (!isContractDeployed) {
82
+ throw new Error(
83
+ `Merkle Distributor contract is not available at address ${incentivesData.contractAddress}`,
84
+ );
85
+ }
86
+
87
+ const { txHash } = await claimIncentivesFromMerkleDistributor({
88
+ signer: this.signer,
89
+ contractAddress: incentivesData.contractAddress,
90
+ index: incentivesData.index,
91
+ operatorAddress: incentivesData.operatorAddress,
92
+ amount: incentivesData.amount,
93
+ merkleProof: incentivesData.merkleProof,
94
+ });
95
+
96
+ return { txHash };
97
+ } catch (error: any) {
98
+ console.log('Error claiming incentives:', error);
99
+ throw new Error(`Failed to claim incentives: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Read isClaimed.
105
+ *
106
+ * @param {ETH_ADDRESS} contractAddress - Address of the Merkle Distributor Contract
107
+ * @param {ETH_ADDRESS} index - operator index in merkle tree
108
+ * @returns {Promise<boolean>} true if incentives are already claime
109
+ *
110
+ */
111
+ async isClaimed(
112
+ contractAddress: ETH_ADDRESS,
113
+ index: number,
114
+ ): Promise<boolean> {
115
+ return await isClaimedFromMerkleDistributor(
116
+ this.chainId,
117
+ contractAddress,
118
+ index,
119
+ this.provider,
120
+ );
121
+ }
122
+
123
+ /**
124
+ * @param address - Operator address
125
+ * @returns {Promise<IncentivesType>} The matched incentives from DB
126
+ * @throws On not found if address not found.
127
+ */
128
+ async getIncentivesByAddress(address: string): Promise<IncentivesType> {
129
+ const incentives: IncentivesType = await this.request(
130
+ `/${DEFAULT_BASE_VERSION}/address/incentives/${address}`,
131
+ {
132
+ method: 'GET',
133
+ },
134
+ );
135
+ return incentives;
136
+ }
137
+ }
@@ -0,0 +1,65 @@
1
+ import { type ETH_ADDRESS } from './types';
2
+ import {
3
+ Contract,
4
+ type JsonRpcApiProvider,
5
+ type JsonRpcProvider,
6
+ type Provider,
7
+ type Signer,
8
+ } from 'ethers';
9
+ import { MerkleDistributorABI } from './abi/MerkleDistributorWithDeadline';
10
+ import { getProvider } from './utils';
11
+
12
+ export const claimIncentivesFromMerkleDistributor = async (incentivesData: {
13
+ signer: Signer;
14
+ contractAddress: ETH_ADDRESS;
15
+ index: number;
16
+ operatorAddress: ETH_ADDRESS;
17
+ amount: number;
18
+ merkleProof: string[];
19
+ }): Promise<{ txHash: string }> => {
20
+ try {
21
+ const contract = new Contract(
22
+ incentivesData.contractAddress,
23
+ MerkleDistributorABI.abi,
24
+ incentivesData.signer,
25
+ );
26
+
27
+ const tx = await contract.claim(
28
+ BigInt(incentivesData.index),
29
+ incentivesData.operatorAddress,
30
+ BigInt(incentivesData.amount),
31
+ incentivesData.merkleProof,
32
+ );
33
+
34
+ const receipt = await tx.wait();
35
+
36
+ return { txHash: receipt.hash };
37
+ } catch (error: any) {
38
+ console.log('Error claiming incentives:', error);
39
+ throw new Error(`Failed to claim incentives: ${error.message}`);
40
+ }
41
+ };
42
+
43
+ export const isClaimedFromMerkleDistributor = async (
44
+ chainId: number,
45
+ contractAddress: ETH_ADDRESS,
46
+ index: number,
47
+ provider: Provider | JsonRpcProvider | JsonRpcApiProvider | undefined | null,
48
+ ): Promise<boolean> => {
49
+ try {
50
+ const clientProvider = provider ?? getProvider(chainId);
51
+
52
+ const contract = new Contract(
53
+ contractAddress,
54
+ MerkleDistributorABI.abi,
55
+ clientProvider,
56
+ );
57
+
58
+ const claimed = await contract.isClaimed(BigInt(index));
59
+
60
+ return claimed;
61
+ } catch (error: any) {
62
+ console.log('Error checking claim status:', error);
63
+ throw new Error(`Failed to check claim status: ${error.message}`);
64
+ }
65
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { ZeroAddress, type Provider, type Signer } from 'ethers';
1
+ import {
2
+ ZeroAddress,
3
+ type Provider,
4
+ type Signer,
5
+ type JsonRpcSigner,
6
+ type JsonRpcProvider,
7
+ type JsonRpcApiProvider,
8
+ } from 'ethers';
2
9
  import { v4 as uuidv4 } from 'uuid';
3
10
  import { Base } from './base.js';
4
11
  import {
@@ -30,7 +37,6 @@ import {
30
37
  type ClusterValidator,
31
38
  type ETH_ADDRESS,
32
39
  type OWRTranches,
33
- type Incentives,
34
40
  } from './types.js';
35
41
  import { clusterConfigOrDefinitionHash } from './verification/common.js';
36
42
  import { validatePayload } from './ajv.js';
@@ -48,6 +54,7 @@ import {
48
54
  getOWRTranches,
49
55
  } from './splitHelpers.js';
50
56
  import { isContractAvailable } from './utils.js';
57
+ import { Incentives } from './incentives.js';
51
58
  export * from './types.js';
52
59
  export * from './services.js';
53
60
  export * from './verification/signature-validator.js';
@@ -57,7 +64,14 @@ export * from './verification/common.js';
57
64
  * Obol sdk Client can be used for creating, managing and activating distributed validators.
58
65
  */
59
66
  export class Client extends Base {
60
- private readonly signer: Signer | undefined;
67
+ private readonly signer: Signer | JsonRpcSigner | undefined;
68
+ public incentives: Incentives;
69
+ public provider:
70
+ | Provider
71
+ | JsonRpcProvider
72
+ | JsonRpcApiProvider
73
+ | undefined
74
+ | null;
61
75
 
62
76
  /**
63
77
  * @param config - Client configurations
@@ -69,9 +83,23 @@ export class Client extends Base {
69
83
  * An example of how to instantiate obol-sdk Client:
70
84
  * [obolClient](https://github.com/ObolNetwork/obol-sdk-examples/blob/main/TS-Example/index.ts#L29)
71
85
  */
72
- constructor(config: { baseUrl?: string; chainId?: number }, signer?: Signer) {
86
+ constructor(
87
+ config: { baseUrl?: string; chainId?: number },
88
+ signer?: Signer | JsonRpcSigner,
89
+ provider?: Provider | JsonRpcProvider,
90
+ ) {
73
91
  super(config);
74
92
  this.signer = signer;
93
+ // Use the provided provider, or fall back to signer.provider if available
94
+ this.provider =
95
+ provider ??
96
+ (signer && 'provider' in signer ? signer.provider : undefined);
97
+ this.incentives = new Incentives(
98
+ this.signer,
99
+ this.chainId,
100
+ this.request.bind(this),
101
+ (this.provider = provider),
102
+ );
75
103
  }
76
104
 
77
105
  /**
@@ -521,19 +549,4 @@ export class Client extends Base {
521
549
  );
522
550
  return lock;
523
551
  }
524
-
525
- /**
526
- * @param address - Operator address
527
- * @returns {Promise<Incentives>} The matched incentives from DB
528
- * @throws On not found if address not found.
529
- */
530
- async getIncentivesByAddress(address: string): Promise<Incentives> {
531
- const incentives: Incentives = await this.request(
532
- `/${DEFAULT_BASE_VERSION}/address/incentives/${address}`,
533
- {
534
- method: 'GET',
535
- },
536
- );
537
- return incentives;
538
- }
539
552
  }
@@ -23,7 +23,7 @@ export const validateAddressSignature = async ({
23
23
  chainId: number;
24
24
  }): Promise<boolean> => {
25
25
  try {
26
- const provider = getProvider(chainId);
26
+ const provider = getProvider(chainId); // [TODO Hanan], should expect it from signer.provider, or passed in client
27
27
  if (provider) {
28
28
  const contractAddress = await isContractAvailable(address, provider);
29
29
  if (contractAddress) {