@chorus-one/polygon 1.0.1 → 1.0.4
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/README.md +21 -4
- package/dist/cjs/constants.d.ts +55 -0
- package/dist/cjs/constants.js +32 -3
- package/dist/cjs/staker.d.ts +5 -4
- package/dist/cjs/staker.js +47 -14
- package/dist/cjs/types.d.ts +8 -0
- package/dist/mjs/constants.d.ts +55 -0
- package/dist/mjs/constants.js +31 -2
- package/dist/mjs/staker.d.ts +5 -4
- package/dist/mjs/staker.js +48 -15
- package/dist/mjs/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/constants.ts +32 -2
- package/src/staker.ts +53 -14
- package/src/types.ts +8 -0
- package/test/fixtures/expected-data.ts +2 -1
- package/test/integration/staker.spec.ts +28 -0
- package/test/lib/networks.json +1 -1
- package/test/staker.spec.ts +17 -0
package/README.md
CHANGED
|
@@ -99,7 +99,9 @@ const { txHash } = await staker.broadcast({ signedTx })
|
|
|
99
99
|
|
|
100
100
|
### Stake (Delegate) to Validator
|
|
101
101
|
|
|
102
|
-
Delegate POL tokens to a validator via their ValidatorShare contract
|
|
102
|
+
Delegate POL tokens to a validator via their ValidatorShare contract.
|
|
103
|
+
|
|
104
|
+
You must provide exactly one of `slippageBps` or `minSharesToMint` (not both). There is no default — omitting both will throw an error.
|
|
103
105
|
|
|
104
106
|
```javascript
|
|
105
107
|
const { tx } = await staker.buildStakeTx({
|
|
@@ -121,7 +123,9 @@ const { txHash } = await staker.broadcast({ signedTx })
|
|
|
121
123
|
|
|
122
124
|
### Unstake (Unbond) from Validator
|
|
123
125
|
|
|
124
|
-
Create an unbond request to unstake POL tokens. After the unbonding period (~80 epochs, approximately 3-4 days), call withdraw to claim funds
|
|
126
|
+
Create an unbond request to unstake POL tokens. After the unbonding period (~80 epochs, approximately 3-4 days), call withdraw to claim funds.
|
|
127
|
+
|
|
128
|
+
You must provide exactly one of `slippageBps` or `maximumSharesToBurn` (not both). There is no default — omitting both will throw an error.
|
|
125
129
|
|
|
126
130
|
```javascript
|
|
127
131
|
const { tx } = await staker.buildUnstakeTx({
|
|
@@ -212,10 +216,10 @@ console.log(status) // 'success', 'failure', or 'unknown'
|
|
|
212
216
|
|
|
213
217
|
## Key Features
|
|
214
218
|
|
|
215
|
-
- **Ethereum L1 Based**: Polygon PoS staking operates via ValidatorShare contracts deployed on Ethereum mainnet (or Sepolia for testnet)
|
|
219
|
+
- **Ethereum L1 Based**: Polygon PoS staking operates via ValidatorShare contracts deployed on Ethereum mainnet (or Sepolia for testnet). Every transaction includes a `chainId` field so you know exactly which chain to broadcast on (`1` for mainnet, `11155111` for Sepolia).
|
|
216
220
|
- **POL Token Staking**: Stake the native POL token (formerly MATIC) to validators
|
|
217
221
|
- **Human-Readable Amounts**: Pass token amounts as strings (e.g., '1.5'), conversion to wei is handled automatically
|
|
218
|
-
- **Slippage Protection**: Stake and unstake operations
|
|
222
|
+
- **Slippage Protection**: Stake and unstake operations require either `slippageBps` (basis points) for automatic slippage calculation, or manual `minSharesToMint`/`maximumSharesToBurn` parameters. Exactly one must be provided — there is no default.
|
|
219
223
|
- **Query Methods**: Read stake balances, rewards, allowances, unbond status (with POL amount and withdrawability), and epoch information
|
|
220
224
|
- **Rewards Management**: Claim rewards to wallet or compound them back into your delegation
|
|
221
225
|
|
|
@@ -227,6 +231,19 @@ console.log(status) // 'success', 'failure', or 'unknown'
|
|
|
227
231
|
- **Referrer Tracking**: Transaction builders that support referrer tracking (stake, unstake, claim rewards, compound) append a tracking marker to the transaction calldata. By default, `sdk-chorusone-staking` is used as the referrer. You can provide a custom referrer via the `referrer` parameter.
|
|
228
232
|
- **Exchange Rate**: The exchange rate between shares and POL may fluctuate. Use `slippageBps` for automatic slippage protection, or specify `minSharesToMint`/`maximumSharesToBurn` directly. Foundation validators (ID < 8) use different precision than others.
|
|
229
233
|
- **Validator Share Contracts**: Each validator has their own ValidatorShare contract address. You must specify the correct contract address for the validator you want to delegate to.
|
|
234
|
+
- **Testnet Validators**: Testnet validators on Sepolia may be locked or inactive. To list all active ValidatorShare addresses, use [Foundry's](https://book.getfoundry.sh/) `cast`:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
export ETH_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
|
|
238
|
+
SM="0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE"
|
|
239
|
+
count=$(cast call $SM "NFTCounter()(uint256)")
|
|
240
|
+
for id in $(seq 1 $((count - 1))); do
|
|
241
|
+
raw=$(cast call $SM "validators(uint256)(uint256,uint256,uint256,uint256,uint256,address,address,uint8,uint256,uint256,uint256,uint256,uint256)" $id 2>/dev/null)
|
|
242
|
+
contract=$(echo "$raw" | sed -n 7p)
|
|
243
|
+
st=$(echo "$raw" | sed -n 8p)
|
|
244
|
+
[ "$st" = "1" ] && echo $contract
|
|
245
|
+
done
|
|
246
|
+
```
|
|
230
247
|
|
|
231
248
|
## License
|
|
232
249
|
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -184,4 +184,59 @@ export declare const STAKE_MANAGER_ABI: readonly [{
|
|
|
184
184
|
readonly internalType: "uint256";
|
|
185
185
|
}];
|
|
186
186
|
readonly stateMutability: "view";
|
|
187
|
+
}, {
|
|
188
|
+
readonly type: "function";
|
|
189
|
+
readonly name: "validators";
|
|
190
|
+
readonly inputs: readonly [{
|
|
191
|
+
readonly name: "";
|
|
192
|
+
readonly type: "uint256";
|
|
193
|
+
readonly internalType: "uint256";
|
|
194
|
+
}];
|
|
195
|
+
readonly outputs: readonly [{
|
|
196
|
+
readonly name: "amount";
|
|
197
|
+
readonly type: "uint256";
|
|
198
|
+
}, {
|
|
199
|
+
readonly name: "reward";
|
|
200
|
+
readonly type: "uint256";
|
|
201
|
+
}, {
|
|
202
|
+
readonly name: "activationEpoch";
|
|
203
|
+
readonly type: "uint256";
|
|
204
|
+
}, {
|
|
205
|
+
readonly name: "deactivationEpoch";
|
|
206
|
+
readonly type: "uint256";
|
|
207
|
+
}, {
|
|
208
|
+
readonly name: "jailTime";
|
|
209
|
+
readonly type: "uint256";
|
|
210
|
+
}, {
|
|
211
|
+
readonly name: "signer";
|
|
212
|
+
readonly type: "address";
|
|
213
|
+
}, {
|
|
214
|
+
readonly name: "contractAddress";
|
|
215
|
+
readonly type: "address";
|
|
216
|
+
}, {
|
|
217
|
+
readonly name: "status";
|
|
218
|
+
readonly type: "uint256";
|
|
219
|
+
}, {
|
|
220
|
+
readonly name: "commissionRate";
|
|
221
|
+
readonly type: "uint256";
|
|
222
|
+
}, {
|
|
223
|
+
readonly name: "lastCommissionUpdate";
|
|
224
|
+
readonly type: "uint256";
|
|
225
|
+
}, {
|
|
226
|
+
readonly name: "delegatorsReward";
|
|
227
|
+
readonly type: "uint256";
|
|
228
|
+
}, {
|
|
229
|
+
readonly name: "delegatedAmount";
|
|
230
|
+
readonly type: "uint256";
|
|
231
|
+
}, {
|
|
232
|
+
readonly name: "initialRewardPerStake";
|
|
233
|
+
readonly type: "uint256";
|
|
234
|
+
}];
|
|
235
|
+
readonly stateMutability: "view";
|
|
187
236
|
}];
|
|
237
|
+
export declare const VALIDATOR_STATUS: {
|
|
238
|
+
readonly 0: "Inactive";
|
|
239
|
+
readonly 1: "Active";
|
|
240
|
+
readonly 2: "Locked";
|
|
241
|
+
readonly 3: "Unstaked";
|
|
242
|
+
};
|
package/dist/cjs/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.STAKE_MANAGER_ABI = exports.EXCHANGE_RATE_HIGH_PRECISION = exports.EXCHANGE_RATE_PRECISION = exports.VALIDATOR_SHARE_ABI = exports.CHORUS_ONE_POLYGON_VALIDATORS = exports.NETWORK_CONTRACTS = void 0;
|
|
3
|
+
exports.VALIDATOR_STATUS = exports.STAKE_MANAGER_ABI = exports.EXCHANGE_RATE_HIGH_PRECISION = exports.EXCHANGE_RATE_PRECISION = exports.VALIDATOR_SHARE_ABI = exports.CHORUS_ONE_POLYGON_VALIDATORS = exports.NETWORK_CONTRACTS = void 0;
|
|
4
4
|
/** Contract addresses per network (mainnet = Ethereum L1, testnet = Sepolia L1) */
|
|
5
5
|
// Reference: https://docs.polygon.technology/pos/reference/rpc-endpoints/
|
|
6
6
|
exports.NETWORK_CONTRACTS = {
|
|
@@ -15,10 +15,12 @@ exports.NETWORK_CONTRACTS = {
|
|
|
15
15
|
};
|
|
16
16
|
/** Chorus One Polygon ValidatorShare contract addresses */
|
|
17
17
|
// Reference mainnet: https://staking.polygon.technology/validators/106
|
|
18
|
-
// Reference testnet
|
|
18
|
+
// Reference testnet: https://staking.polygon.technology/validators/31
|
|
19
|
+
// Note: Testnet validators may be locked. To list active validators, query the StakeManager
|
|
20
|
+
// contract on Sepolia: https://sepolia.etherscan.io/address/0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE
|
|
19
21
|
exports.CHORUS_ONE_POLYGON_VALIDATORS = {
|
|
20
22
|
mainnet: '0xD9E6987D77bf2c6d0647b8181fd68A259f838C36',
|
|
21
|
-
testnet: '
|
|
23
|
+
testnet: '0xe50f5ad9b885675fd11d8204eb01c83a8a32a91d'
|
|
22
24
|
};
|
|
23
25
|
// Reference: https://github.com/0xPolygon/pos-contracts/blob/main/contracts/staking/validatorShare/ValidatorShare.sol
|
|
24
26
|
exports.VALIDATOR_SHARE_ABI = [
|
|
@@ -137,5 +139,32 @@ exports.STAKE_MANAGER_ABI = [
|
|
|
137
139
|
inputs: [],
|
|
138
140
|
outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
139
141
|
stateMutability: 'view'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: 'function',
|
|
145
|
+
name: 'validators',
|
|
146
|
+
inputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
147
|
+
outputs: [
|
|
148
|
+
{ name: 'amount', type: 'uint256' },
|
|
149
|
+
{ name: 'reward', type: 'uint256' },
|
|
150
|
+
{ name: 'activationEpoch', type: 'uint256' },
|
|
151
|
+
{ name: 'deactivationEpoch', type: 'uint256' },
|
|
152
|
+
{ name: 'jailTime', type: 'uint256' },
|
|
153
|
+
{ name: 'signer', type: 'address' },
|
|
154
|
+
{ name: 'contractAddress', type: 'address' },
|
|
155
|
+
{ name: 'status', type: 'uint256' },
|
|
156
|
+
{ name: 'commissionRate', type: 'uint256' },
|
|
157
|
+
{ name: 'lastCommissionUpdate', type: 'uint256' },
|
|
158
|
+
{ name: 'delegatorsReward', type: 'uint256' },
|
|
159
|
+
{ name: 'delegatedAmount', type: 'uint256' },
|
|
160
|
+
{ name: 'initialRewardPerStake', type: 'uint256' }
|
|
161
|
+
],
|
|
162
|
+
stateMutability: 'view'
|
|
140
163
|
}
|
|
141
164
|
];
|
|
165
|
+
exports.VALIDATOR_STATUS = {
|
|
166
|
+
0: 'Inactive',
|
|
167
|
+
1: 'Active',
|
|
168
|
+
2: 'Locked',
|
|
169
|
+
3: 'Unstaked'
|
|
170
|
+
};
|
package/dist/cjs/staker.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare class PolygonStaker {
|
|
|
54
54
|
* @param params.delegatorAddress - The delegator's Ethereum address
|
|
55
55
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
56
56
|
* @param params.amount - The amount to stake in POL
|
|
57
|
-
* @param params.slippageBps -
|
|
58
|
-
* @param params.minSharesToMint -
|
|
57
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate minSharesToMint. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
58
|
+
* @param params.minSharesToMint - Minimum validator shares to receive. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
59
59
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
60
60
|
*
|
|
61
61
|
* @returns Returns a promise that resolves to a Polygon staking transaction
|
|
@@ -80,8 +80,8 @@ export declare class PolygonStaker {
|
|
|
80
80
|
* @param params.delegatorAddress - The delegator's address
|
|
81
81
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
82
82
|
* @param params.amount - The amount to unstake in POL (will be converted to wei internally)
|
|
83
|
-
* @param params.slippageBps -
|
|
84
|
-
* @param params.maximumSharesToBurn -
|
|
83
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate maximumSharesToBurn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
84
|
+
* @param params.maximumSharesToBurn - Maximum validator shares willing to burn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
85
85
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
86
86
|
*
|
|
87
87
|
* @returns Returns a promise that resolves to a Polygon unstaking transaction
|
|
@@ -331,5 +331,6 @@ export declare class PolygonStaker {
|
|
|
331
331
|
getTxStatus(params: {
|
|
332
332
|
txHash: Hex;
|
|
333
333
|
}): Promise<PolygonTxStatus>;
|
|
334
|
+
private assertValidatorActive;
|
|
334
335
|
private parseAmount;
|
|
335
336
|
}
|
package/dist/cjs/staker.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PolygonStaker = void 0;
|
|
4
4
|
const viem_1 = require("viem");
|
|
5
5
|
const chains_1 = require("viem/chains");
|
|
6
|
-
const
|
|
6
|
+
const secp256k1_1 = require("@noble/curves/secp256k1");
|
|
7
7
|
const referrer_1 = require("./referrer");
|
|
8
8
|
const constants_1 = require("./constants");
|
|
9
9
|
/**
|
|
@@ -46,7 +46,8 @@ class PolygonStaker {
|
|
|
46
46
|
* @returns Returns an array containing the derived address.
|
|
47
47
|
*/
|
|
48
48
|
static getAddressDerivationFn = () => async (publicKey) => {
|
|
49
|
-
const
|
|
49
|
+
const point = secp256k1_1.secp256k1.Point.fromHex(publicKey);
|
|
50
|
+
const pkUncompressed = point.toBytes(false);
|
|
50
51
|
const hash = (0, viem_1.keccak256)(pkUncompressed.subarray(1));
|
|
51
52
|
const ethAddress = hash.slice(-40);
|
|
52
53
|
return [ethAddress];
|
|
@@ -94,7 +95,8 @@ class PolygonStaker {
|
|
|
94
95
|
tx: {
|
|
95
96
|
to: this.contracts.stakingTokenAddress,
|
|
96
97
|
data,
|
|
97
|
-
value: 0n
|
|
98
|
+
value: 0n,
|
|
99
|
+
chainId: this.chain.id
|
|
98
100
|
}
|
|
99
101
|
};
|
|
100
102
|
}
|
|
@@ -108,8 +110,8 @@ class PolygonStaker {
|
|
|
108
110
|
* @param params.delegatorAddress - The delegator's Ethereum address
|
|
109
111
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
110
112
|
* @param params.amount - The amount to stake in POL
|
|
111
|
-
* @param params.slippageBps -
|
|
112
|
-
* @param params.minSharesToMint -
|
|
113
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate minSharesToMint. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
114
|
+
* @param params.minSharesToMint - Minimum validator shares to receive. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
113
115
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
114
116
|
*
|
|
115
117
|
* @returns Returns a promise that resolves to a Polygon staking transaction
|
|
@@ -127,7 +129,10 @@ class PolygonStaker {
|
|
|
127
129
|
throw new Error('Cannot specify both slippageBps and minSharesToMint. Use one or the other.');
|
|
128
130
|
}
|
|
129
131
|
const amountWei = this.parseAmount(amount);
|
|
130
|
-
const allowance = await
|
|
132
|
+
const [allowance] = await Promise.all([
|
|
133
|
+
this.getAllowance(delegatorAddress),
|
|
134
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
135
|
+
]);
|
|
131
136
|
if ((0, viem_1.parseEther)(allowance) < amountWei) {
|
|
132
137
|
throw new Error(`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`);
|
|
133
138
|
}
|
|
@@ -151,7 +156,8 @@ class PolygonStaker {
|
|
|
151
156
|
tx: {
|
|
152
157
|
to: validatorShareAddress,
|
|
153
158
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
154
|
-
value: 0n
|
|
159
|
+
value: 0n,
|
|
160
|
+
chainId: this.chain.id
|
|
155
161
|
}
|
|
156
162
|
};
|
|
157
163
|
}
|
|
@@ -165,8 +171,8 @@ class PolygonStaker {
|
|
|
165
171
|
* @param params.delegatorAddress - The delegator's address
|
|
166
172
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
167
173
|
* @param params.amount - The amount to unstake in POL (will be converted to wei internally)
|
|
168
|
-
* @param params.slippageBps -
|
|
169
|
-
* @param params.maximumSharesToBurn -
|
|
174
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate maximumSharesToBurn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
175
|
+
* @param params.maximumSharesToBurn - Maximum validator shares willing to burn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
170
176
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
171
177
|
*
|
|
172
178
|
* @returns Returns a promise that resolves to a Polygon unstaking transaction
|
|
@@ -205,7 +211,8 @@ class PolygonStaker {
|
|
|
205
211
|
tx: {
|
|
206
212
|
to: validatorShareAddress,
|
|
207
213
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
208
|
-
value: 0n
|
|
214
|
+
value: 0n,
|
|
215
|
+
chainId: this.chain.id
|
|
209
216
|
}
|
|
210
217
|
};
|
|
211
218
|
}
|
|
@@ -252,7 +259,8 @@ class PolygonStaker {
|
|
|
252
259
|
tx: {
|
|
253
260
|
to: validatorShareAddress,
|
|
254
261
|
data,
|
|
255
|
-
value: 0n
|
|
262
|
+
value: 0n,
|
|
263
|
+
chainId: this.chain.id
|
|
256
264
|
}
|
|
257
265
|
};
|
|
258
266
|
}
|
|
@@ -288,7 +296,8 @@ class PolygonStaker {
|
|
|
288
296
|
tx: {
|
|
289
297
|
to: validatorShareAddress,
|
|
290
298
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
291
|
-
value: 0n
|
|
299
|
+
value: 0n,
|
|
300
|
+
chainId: this.chain.id
|
|
292
301
|
}
|
|
293
302
|
};
|
|
294
303
|
}
|
|
@@ -312,7 +321,10 @@ class PolygonStaker {
|
|
|
312
321
|
if (!(0, viem_1.isAddress)(validatorShareAddress)) {
|
|
313
322
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`);
|
|
314
323
|
}
|
|
315
|
-
const rewards = await
|
|
324
|
+
const [rewards] = await Promise.all([
|
|
325
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
326
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
327
|
+
]);
|
|
316
328
|
if ((0, viem_1.parseEther)(rewards) === 0n) {
|
|
317
329
|
throw new Error('No rewards available to compound');
|
|
318
330
|
}
|
|
@@ -324,7 +336,8 @@ class PolygonStaker {
|
|
|
324
336
|
tx: {
|
|
325
337
|
to: validatorShareAddress,
|
|
326
338
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
327
|
-
value: 0n
|
|
339
|
+
value: 0n,
|
|
340
|
+
chainId: this.chain.id
|
|
328
341
|
}
|
|
329
342
|
};
|
|
330
343
|
}
|
|
@@ -692,6 +705,26 @@ class PolygonStaker {
|
|
|
692
705
|
};
|
|
693
706
|
}
|
|
694
707
|
}
|
|
708
|
+
async assertValidatorActive(validatorShareAddress) {
|
|
709
|
+
const validatorId = await this.publicClient.readContract({
|
|
710
|
+
address: validatorShareAddress,
|
|
711
|
+
abi: constants_1.VALIDATOR_SHARE_ABI,
|
|
712
|
+
functionName: 'validatorId'
|
|
713
|
+
});
|
|
714
|
+
const validator = await this.publicClient.readContract({
|
|
715
|
+
address: this.contracts.stakeManagerAddress,
|
|
716
|
+
abi: constants_1.STAKE_MANAGER_ABI,
|
|
717
|
+
functionName: 'validators',
|
|
718
|
+
args: [validatorId]
|
|
719
|
+
});
|
|
720
|
+
const status = Number(validator[7]);
|
|
721
|
+
if (status !== 1) {
|
|
722
|
+
const statusName = constants_1.VALIDATOR_STATUS[status] ?? 'Unknown';
|
|
723
|
+
throw new Error(`Validator is not active (status: ${statusName}). ` +
|
|
724
|
+
'The validator may be locked, inactive, or unstaked. ' +
|
|
725
|
+
'See the @chorus-one/polygon README for how to list active validators on testnet.');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
695
728
|
parseAmount(amount) {
|
|
696
729
|
if (typeof amount === 'bigint') {
|
|
697
730
|
throw new Error('Amount must be a string, denominated in POL. e.g. "1.5" - 1.5 POL. You can use `formatEther` to convert a `bigint` to a string');
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface Transaction {
|
|
|
13
13
|
data: Hex;
|
|
14
14
|
/** The amount of ETH (in wei) to be sent with the transaction */
|
|
15
15
|
value?: bigint;
|
|
16
|
+
/**
|
|
17
|
+
* The chain ID where the transaction must be broadcast.
|
|
18
|
+
*
|
|
19
|
+
* Polygon staking contracts live on Ethereum L1, not on the Polygon chain itself:
|
|
20
|
+
* - mainnet: `1` (Ethereum Mainnet)
|
|
21
|
+
* - testnet: `11155111` (Sepolia)
|
|
22
|
+
*/
|
|
23
|
+
chainId: number;
|
|
16
24
|
}
|
|
17
25
|
export interface PolygonTxStatus {
|
|
18
26
|
/** Status of the transaction */
|
package/dist/mjs/constants.d.ts
CHANGED
|
@@ -184,4 +184,59 @@ export declare const STAKE_MANAGER_ABI: readonly [{
|
|
|
184
184
|
readonly internalType: "uint256";
|
|
185
185
|
}];
|
|
186
186
|
readonly stateMutability: "view";
|
|
187
|
+
}, {
|
|
188
|
+
readonly type: "function";
|
|
189
|
+
readonly name: "validators";
|
|
190
|
+
readonly inputs: readonly [{
|
|
191
|
+
readonly name: "";
|
|
192
|
+
readonly type: "uint256";
|
|
193
|
+
readonly internalType: "uint256";
|
|
194
|
+
}];
|
|
195
|
+
readonly outputs: readonly [{
|
|
196
|
+
readonly name: "amount";
|
|
197
|
+
readonly type: "uint256";
|
|
198
|
+
}, {
|
|
199
|
+
readonly name: "reward";
|
|
200
|
+
readonly type: "uint256";
|
|
201
|
+
}, {
|
|
202
|
+
readonly name: "activationEpoch";
|
|
203
|
+
readonly type: "uint256";
|
|
204
|
+
}, {
|
|
205
|
+
readonly name: "deactivationEpoch";
|
|
206
|
+
readonly type: "uint256";
|
|
207
|
+
}, {
|
|
208
|
+
readonly name: "jailTime";
|
|
209
|
+
readonly type: "uint256";
|
|
210
|
+
}, {
|
|
211
|
+
readonly name: "signer";
|
|
212
|
+
readonly type: "address";
|
|
213
|
+
}, {
|
|
214
|
+
readonly name: "contractAddress";
|
|
215
|
+
readonly type: "address";
|
|
216
|
+
}, {
|
|
217
|
+
readonly name: "status";
|
|
218
|
+
readonly type: "uint256";
|
|
219
|
+
}, {
|
|
220
|
+
readonly name: "commissionRate";
|
|
221
|
+
readonly type: "uint256";
|
|
222
|
+
}, {
|
|
223
|
+
readonly name: "lastCommissionUpdate";
|
|
224
|
+
readonly type: "uint256";
|
|
225
|
+
}, {
|
|
226
|
+
readonly name: "delegatorsReward";
|
|
227
|
+
readonly type: "uint256";
|
|
228
|
+
}, {
|
|
229
|
+
readonly name: "delegatedAmount";
|
|
230
|
+
readonly type: "uint256";
|
|
231
|
+
}, {
|
|
232
|
+
readonly name: "initialRewardPerStake";
|
|
233
|
+
readonly type: "uint256";
|
|
234
|
+
}];
|
|
235
|
+
readonly stateMutability: "view";
|
|
187
236
|
}];
|
|
237
|
+
export declare const VALIDATOR_STATUS: {
|
|
238
|
+
readonly 0: "Inactive";
|
|
239
|
+
readonly 1: "Active";
|
|
240
|
+
readonly 2: "Locked";
|
|
241
|
+
readonly 3: "Unstaked";
|
|
242
|
+
};
|
package/dist/mjs/constants.js
CHANGED
|
@@ -12,10 +12,12 @@ export const NETWORK_CONTRACTS = {
|
|
|
12
12
|
};
|
|
13
13
|
/** Chorus One Polygon ValidatorShare contract addresses */
|
|
14
14
|
// Reference mainnet: https://staking.polygon.technology/validators/106
|
|
15
|
-
// Reference testnet
|
|
15
|
+
// Reference testnet: https://staking.polygon.technology/validators/31
|
|
16
|
+
// Note: Testnet validators may be locked. To list active validators, query the StakeManager
|
|
17
|
+
// contract on Sepolia: https://sepolia.etherscan.io/address/0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE
|
|
16
18
|
export const CHORUS_ONE_POLYGON_VALIDATORS = {
|
|
17
19
|
mainnet: '0xD9E6987D77bf2c6d0647b8181fd68A259f838C36',
|
|
18
|
-
testnet: '
|
|
20
|
+
testnet: '0xe50f5ad9b885675fd11d8204eb01c83a8a32a91d'
|
|
19
21
|
};
|
|
20
22
|
// Reference: https://github.com/0xPolygon/pos-contracts/blob/main/contracts/staking/validatorShare/ValidatorShare.sol
|
|
21
23
|
export const VALIDATOR_SHARE_ABI = [
|
|
@@ -134,5 +136,32 @@ export const STAKE_MANAGER_ABI = [
|
|
|
134
136
|
inputs: [],
|
|
135
137
|
outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
136
138
|
stateMutability: 'view'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'function',
|
|
142
|
+
name: 'validators',
|
|
143
|
+
inputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
144
|
+
outputs: [
|
|
145
|
+
{ name: 'amount', type: 'uint256' },
|
|
146
|
+
{ name: 'reward', type: 'uint256' },
|
|
147
|
+
{ name: 'activationEpoch', type: 'uint256' },
|
|
148
|
+
{ name: 'deactivationEpoch', type: 'uint256' },
|
|
149
|
+
{ name: 'jailTime', type: 'uint256' },
|
|
150
|
+
{ name: 'signer', type: 'address' },
|
|
151
|
+
{ name: 'contractAddress', type: 'address' },
|
|
152
|
+
{ name: 'status', type: 'uint256' },
|
|
153
|
+
{ name: 'commissionRate', type: 'uint256' },
|
|
154
|
+
{ name: 'lastCommissionUpdate', type: 'uint256' },
|
|
155
|
+
{ name: 'delegatorsReward', type: 'uint256' },
|
|
156
|
+
{ name: 'delegatedAmount', type: 'uint256' },
|
|
157
|
+
{ name: 'initialRewardPerStake', type: 'uint256' }
|
|
158
|
+
],
|
|
159
|
+
stateMutability: 'view'
|
|
137
160
|
}
|
|
138
161
|
];
|
|
162
|
+
export const VALIDATOR_STATUS = {
|
|
163
|
+
0: 'Inactive',
|
|
164
|
+
1: 'Active',
|
|
165
|
+
2: 'Locked',
|
|
166
|
+
3: 'Unstaked'
|
|
167
|
+
};
|
package/dist/mjs/staker.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export declare class PolygonStaker {
|
|
|
54
54
|
* @param params.delegatorAddress - The delegator's Ethereum address
|
|
55
55
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
56
56
|
* @param params.amount - The amount to stake in POL
|
|
57
|
-
* @param params.slippageBps -
|
|
58
|
-
* @param params.minSharesToMint -
|
|
57
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate minSharesToMint. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
58
|
+
* @param params.minSharesToMint - Minimum validator shares to receive. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
59
59
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
60
60
|
*
|
|
61
61
|
* @returns Returns a promise that resolves to a Polygon staking transaction
|
|
@@ -80,8 +80,8 @@ export declare class PolygonStaker {
|
|
|
80
80
|
* @param params.delegatorAddress - The delegator's address
|
|
81
81
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
82
82
|
* @param params.amount - The amount to unstake in POL (will be converted to wei internally)
|
|
83
|
-
* @param params.slippageBps -
|
|
84
|
-
* @param params.maximumSharesToBurn -
|
|
83
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate maximumSharesToBurn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
84
|
+
* @param params.maximumSharesToBurn - Maximum validator shares willing to burn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
85
85
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
86
86
|
*
|
|
87
87
|
* @returns Returns a promise that resolves to a Polygon unstaking transaction
|
|
@@ -331,5 +331,6 @@ export declare class PolygonStaker {
|
|
|
331
331
|
getTxStatus(params: {
|
|
332
332
|
txHash: Hex;
|
|
333
333
|
}): Promise<PolygonTxStatus>;
|
|
334
|
+
private assertValidatorActive;
|
|
334
335
|
private parseAmount;
|
|
335
336
|
}
|
package/dist/mjs/staker.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createPublicClient, http, encodeFunctionData, parseEther, formatEther, isAddress, keccak256, serializeTransaction, createWalletClient, maxUint256, erc20Abi } from 'viem';
|
|
2
2
|
import { mainnet, sepolia } from 'viem/chains';
|
|
3
|
-
import { secp256k1 } from '@noble/curves/secp256k1
|
|
3
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
4
4
|
import { appendReferrerTracking } from './referrer';
|
|
5
|
-
import { VALIDATOR_SHARE_ABI, STAKE_MANAGER_ABI, NETWORK_CONTRACTS, EXCHANGE_RATE_PRECISION, EXCHANGE_RATE_HIGH_PRECISION } from './constants';
|
|
5
|
+
import { VALIDATOR_SHARE_ABI, STAKE_MANAGER_ABI, NETWORK_CONTRACTS, EXCHANGE_RATE_PRECISION, EXCHANGE_RATE_HIGH_PRECISION, VALIDATOR_STATUS } from './constants';
|
|
6
6
|
/**
|
|
7
7
|
* PolygonStaker - TypeScript SDK for Polygon PoS staking operations
|
|
8
8
|
*
|
|
@@ -43,7 +43,8 @@ export class PolygonStaker {
|
|
|
43
43
|
* @returns Returns an array containing the derived address.
|
|
44
44
|
*/
|
|
45
45
|
static getAddressDerivationFn = () => async (publicKey) => {
|
|
46
|
-
const
|
|
46
|
+
const point = secp256k1.Point.fromHex(publicKey);
|
|
47
|
+
const pkUncompressed = point.toBytes(false);
|
|
47
48
|
const hash = keccak256(pkUncompressed.subarray(1));
|
|
48
49
|
const ethAddress = hash.slice(-40);
|
|
49
50
|
return [ethAddress];
|
|
@@ -91,7 +92,8 @@ export class PolygonStaker {
|
|
|
91
92
|
tx: {
|
|
92
93
|
to: this.contracts.stakingTokenAddress,
|
|
93
94
|
data,
|
|
94
|
-
value: 0n
|
|
95
|
+
value: 0n,
|
|
96
|
+
chainId: this.chain.id
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
99
|
}
|
|
@@ -105,8 +107,8 @@ export class PolygonStaker {
|
|
|
105
107
|
* @param params.delegatorAddress - The delegator's Ethereum address
|
|
106
108
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
107
109
|
* @param params.amount - The amount to stake in POL
|
|
108
|
-
* @param params.slippageBps -
|
|
109
|
-
* @param params.minSharesToMint -
|
|
110
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate minSharesToMint. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
111
|
+
* @param params.minSharesToMint - Minimum validator shares to receive. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
110
112
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
111
113
|
*
|
|
112
114
|
* @returns Returns a promise that resolves to a Polygon staking transaction
|
|
@@ -124,7 +126,10 @@ export class PolygonStaker {
|
|
|
124
126
|
throw new Error('Cannot specify both slippageBps and minSharesToMint. Use one or the other.');
|
|
125
127
|
}
|
|
126
128
|
const amountWei = this.parseAmount(amount);
|
|
127
|
-
const allowance = await
|
|
129
|
+
const [allowance] = await Promise.all([
|
|
130
|
+
this.getAllowance(delegatorAddress),
|
|
131
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
132
|
+
]);
|
|
128
133
|
if (parseEther(allowance) < amountWei) {
|
|
129
134
|
throw new Error(`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`);
|
|
130
135
|
}
|
|
@@ -148,7 +153,8 @@ export class PolygonStaker {
|
|
|
148
153
|
tx: {
|
|
149
154
|
to: validatorShareAddress,
|
|
150
155
|
data: appendReferrerTracking(calldata, referrer),
|
|
151
|
-
value: 0n
|
|
156
|
+
value: 0n,
|
|
157
|
+
chainId: this.chain.id
|
|
152
158
|
}
|
|
153
159
|
};
|
|
154
160
|
}
|
|
@@ -162,8 +168,8 @@ export class PolygonStaker {
|
|
|
162
168
|
* @param params.delegatorAddress - The delegator's address
|
|
163
169
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
164
170
|
* @param params.amount - The amount to unstake in POL (will be converted to wei internally)
|
|
165
|
-
* @param params.slippageBps -
|
|
166
|
-
* @param params.maximumSharesToBurn -
|
|
171
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate maximumSharesToBurn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
172
|
+
* @param params.maximumSharesToBurn - Maximum validator shares willing to burn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
167
173
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
168
174
|
*
|
|
169
175
|
* @returns Returns a promise that resolves to a Polygon unstaking transaction
|
|
@@ -202,7 +208,8 @@ export class PolygonStaker {
|
|
|
202
208
|
tx: {
|
|
203
209
|
to: validatorShareAddress,
|
|
204
210
|
data: appendReferrerTracking(calldata, referrer),
|
|
205
|
-
value: 0n
|
|
211
|
+
value: 0n,
|
|
212
|
+
chainId: this.chain.id
|
|
206
213
|
}
|
|
207
214
|
};
|
|
208
215
|
}
|
|
@@ -249,7 +256,8 @@ export class PolygonStaker {
|
|
|
249
256
|
tx: {
|
|
250
257
|
to: validatorShareAddress,
|
|
251
258
|
data,
|
|
252
|
-
value: 0n
|
|
259
|
+
value: 0n,
|
|
260
|
+
chainId: this.chain.id
|
|
253
261
|
}
|
|
254
262
|
};
|
|
255
263
|
}
|
|
@@ -285,7 +293,8 @@ export class PolygonStaker {
|
|
|
285
293
|
tx: {
|
|
286
294
|
to: validatorShareAddress,
|
|
287
295
|
data: appendReferrerTracking(calldata, referrer),
|
|
288
|
-
value: 0n
|
|
296
|
+
value: 0n,
|
|
297
|
+
chainId: this.chain.id
|
|
289
298
|
}
|
|
290
299
|
};
|
|
291
300
|
}
|
|
@@ -309,7 +318,10 @@ export class PolygonStaker {
|
|
|
309
318
|
if (!isAddress(validatorShareAddress)) {
|
|
310
319
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`);
|
|
311
320
|
}
|
|
312
|
-
const rewards = await
|
|
321
|
+
const [rewards] = await Promise.all([
|
|
322
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
323
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
324
|
+
]);
|
|
313
325
|
if (parseEther(rewards) === 0n) {
|
|
314
326
|
throw new Error('No rewards available to compound');
|
|
315
327
|
}
|
|
@@ -321,7 +333,8 @@ export class PolygonStaker {
|
|
|
321
333
|
tx: {
|
|
322
334
|
to: validatorShareAddress,
|
|
323
335
|
data: appendReferrerTracking(calldata, referrer),
|
|
324
|
-
value: 0n
|
|
336
|
+
value: 0n,
|
|
337
|
+
chainId: this.chain.id
|
|
325
338
|
}
|
|
326
339
|
};
|
|
327
340
|
}
|
|
@@ -689,6 +702,26 @@ export class PolygonStaker {
|
|
|
689
702
|
};
|
|
690
703
|
}
|
|
691
704
|
}
|
|
705
|
+
async assertValidatorActive(validatorShareAddress) {
|
|
706
|
+
const validatorId = await this.publicClient.readContract({
|
|
707
|
+
address: validatorShareAddress,
|
|
708
|
+
abi: VALIDATOR_SHARE_ABI,
|
|
709
|
+
functionName: 'validatorId'
|
|
710
|
+
});
|
|
711
|
+
const validator = await this.publicClient.readContract({
|
|
712
|
+
address: this.contracts.stakeManagerAddress,
|
|
713
|
+
abi: STAKE_MANAGER_ABI,
|
|
714
|
+
functionName: 'validators',
|
|
715
|
+
args: [validatorId]
|
|
716
|
+
});
|
|
717
|
+
const status = Number(validator[7]);
|
|
718
|
+
if (status !== 1) {
|
|
719
|
+
const statusName = VALIDATOR_STATUS[status] ?? 'Unknown';
|
|
720
|
+
throw new Error(`Validator is not active (status: ${statusName}). ` +
|
|
721
|
+
'The validator may be locked, inactive, or unstaked. ' +
|
|
722
|
+
'See the @chorus-one/polygon README for how to list active validators on testnet.');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
692
725
|
parseAmount(amount) {
|
|
693
726
|
if (typeof amount === 'bigint') {
|
|
694
727
|
throw new Error('Amount must be a string, denominated in POL. e.g. "1.5" - 1.5 POL. You can use `formatEther` to convert a `bigint` to a string');
|
package/dist/mjs/types.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface Transaction {
|
|
|
13
13
|
data: Hex;
|
|
14
14
|
/** The amount of ETH (in wei) to be sent with the transaction */
|
|
15
15
|
value?: bigint;
|
|
16
|
+
/**
|
|
17
|
+
* The chain ID where the transaction must be broadcast.
|
|
18
|
+
*
|
|
19
|
+
* Polygon staking contracts live on Ethereum L1, not on the Polygon chain itself:
|
|
20
|
+
* - mainnet: `1` (Ethereum Mainnet)
|
|
21
|
+
* - testnet: `11155111` (Sepolia)
|
|
22
|
+
*/
|
|
23
|
+
chainId: number;
|
|
16
24
|
}
|
|
17
25
|
export interface PolygonTxStatus {
|
|
18
26
|
/** Status of the transaction */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chorus-one/polygon",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "All-in-one toolkit for building staking dApps on Polygon network",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "rm -fr dist/* && tsc -p tsconfig.mjs.json --outDir dist/mjs && tsc -p tsconfig.cjs.json --outDir dist/cjs && bash ../../scripts/fix-package-json",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"type": "module",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@chorus-one/signer": "^1.0.0",
|
|
41
|
-
"@noble/curves": "^
|
|
41
|
+
"@noble/curves": "^1.9.2",
|
|
42
42
|
"viem": "^2.28.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/constants.ts
CHANGED
|
@@ -22,10 +22,12 @@ export const NETWORK_CONTRACTS: Record<PolygonNetworks, NetworkContracts> = {
|
|
|
22
22
|
|
|
23
23
|
/** Chorus One Polygon ValidatorShare contract addresses */
|
|
24
24
|
// Reference mainnet: https://staking.polygon.technology/validators/106
|
|
25
|
-
// Reference testnet
|
|
25
|
+
// Reference testnet: https://staking.polygon.technology/validators/31
|
|
26
|
+
// Note: Testnet validators may be locked. To list active validators, query the StakeManager
|
|
27
|
+
// contract on Sepolia: https://sepolia.etherscan.io/address/0x4AE8f648B1Ec892B6cc68C89cc088583964d08bE
|
|
26
28
|
export const CHORUS_ONE_POLYGON_VALIDATORS = {
|
|
27
29
|
mainnet: '0xD9E6987D77bf2c6d0647b8181fd68A259f838C36' as Address,
|
|
28
|
-
testnet: '
|
|
30
|
+
testnet: '0xe50f5ad9b885675fd11d8204eb01c83a8a32a91d' as Address
|
|
29
31
|
} as const
|
|
30
32
|
|
|
31
33
|
// Reference: https://github.com/0xPolygon/pos-contracts/blob/main/contracts/staking/validatorShare/ValidatorShare.sol
|
|
@@ -147,5 +149,33 @@ export const STAKE_MANAGER_ABI = [
|
|
|
147
149
|
inputs: [],
|
|
148
150
|
outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
149
151
|
stateMutability: 'view'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: 'function',
|
|
155
|
+
name: 'validators',
|
|
156
|
+
inputs: [{ name: '', type: 'uint256', internalType: 'uint256' }],
|
|
157
|
+
outputs: [
|
|
158
|
+
{ name: 'amount', type: 'uint256' },
|
|
159
|
+
{ name: 'reward', type: 'uint256' },
|
|
160
|
+
{ name: 'activationEpoch', type: 'uint256' },
|
|
161
|
+
{ name: 'deactivationEpoch', type: 'uint256' },
|
|
162
|
+
{ name: 'jailTime', type: 'uint256' },
|
|
163
|
+
{ name: 'signer', type: 'address' },
|
|
164
|
+
{ name: 'contractAddress', type: 'address' },
|
|
165
|
+
{ name: 'status', type: 'uint256' },
|
|
166
|
+
{ name: 'commissionRate', type: 'uint256' },
|
|
167
|
+
{ name: 'lastCommissionUpdate', type: 'uint256' },
|
|
168
|
+
{ name: 'delegatorsReward', type: 'uint256' },
|
|
169
|
+
{ name: 'delegatedAmount', type: 'uint256' },
|
|
170
|
+
{ name: 'initialRewardPerStake', type: 'uint256' }
|
|
171
|
+
],
|
|
172
|
+
stateMutability: 'view'
|
|
150
173
|
}
|
|
151
174
|
] as const
|
|
175
|
+
|
|
176
|
+
export const VALIDATOR_STATUS = {
|
|
177
|
+
0: 'Inactive',
|
|
178
|
+
1: 'Active',
|
|
179
|
+
2: 'Locked',
|
|
180
|
+
3: 'Unstaked'
|
|
181
|
+
} as const
|
package/src/staker.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type Chain
|
|
17
17
|
} from 'viem'
|
|
18
18
|
import { mainnet, sepolia } from 'viem/chains'
|
|
19
|
-
import { secp256k1 } from '@noble/curves/secp256k1
|
|
19
|
+
import { secp256k1 } from '@noble/curves/secp256k1'
|
|
20
20
|
import type { Signer } from '@chorus-one/signer'
|
|
21
21
|
import type { Transaction, PolygonNetworkConfig, PolygonTxStatus, StakeInfo, UnbondInfo } from './types'
|
|
22
22
|
import { appendReferrerTracking } from './referrer'
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
NETWORK_CONTRACTS,
|
|
27
27
|
EXCHANGE_RATE_PRECISION,
|
|
28
28
|
EXCHANGE_RATE_HIGH_PRECISION,
|
|
29
|
+
VALIDATOR_STATUS,
|
|
29
30
|
type PolygonNetworks,
|
|
30
31
|
type NetworkContracts
|
|
31
32
|
} from './constants'
|
|
@@ -75,7 +76,8 @@ export class PolygonStaker {
|
|
|
75
76
|
static getAddressDerivationFn =
|
|
76
77
|
() =>
|
|
77
78
|
async (publicKey: Uint8Array): Promise<Array<string>> => {
|
|
78
|
-
const
|
|
79
|
+
const point = secp256k1.Point.fromHex(publicKey)
|
|
80
|
+
const pkUncompressed = point.toBytes(false)
|
|
79
81
|
const hash = keccak256(pkUncompressed.subarray(1))
|
|
80
82
|
const ethAddress = hash.slice(-40)
|
|
81
83
|
return [ethAddress]
|
|
@@ -129,7 +131,8 @@ export class PolygonStaker {
|
|
|
129
131
|
tx: {
|
|
130
132
|
to: this.contracts.stakingTokenAddress,
|
|
131
133
|
data,
|
|
132
|
-
value: 0n
|
|
134
|
+
value: 0n,
|
|
135
|
+
chainId: this.chain.id
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
@@ -144,8 +147,8 @@ export class PolygonStaker {
|
|
|
144
147
|
* @param params.delegatorAddress - The delegator's Ethereum address
|
|
145
148
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
146
149
|
* @param params.amount - The amount to stake in POL
|
|
147
|
-
* @param params.slippageBps -
|
|
148
|
-
* @param params.minSharesToMint -
|
|
150
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate minSharesToMint. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
151
|
+
* @param params.minSharesToMint - Minimum validator shares to receive. Exactly one of `slippageBps` or `minSharesToMint` must be provided (not both, no default).
|
|
149
152
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
150
153
|
*
|
|
151
154
|
* @returns Returns a promise that resolves to a Polygon staking transaction
|
|
@@ -173,7 +176,10 @@ export class PolygonStaker {
|
|
|
173
176
|
|
|
174
177
|
const amountWei = this.parseAmount(amount)
|
|
175
178
|
|
|
176
|
-
const allowance = await
|
|
179
|
+
const [allowance] = await Promise.all([
|
|
180
|
+
this.getAllowance(delegatorAddress),
|
|
181
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
182
|
+
])
|
|
177
183
|
if (parseEther(allowance) < amountWei) {
|
|
178
184
|
throw new Error(
|
|
179
185
|
`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`
|
|
@@ -203,7 +209,8 @@ export class PolygonStaker {
|
|
|
203
209
|
tx: {
|
|
204
210
|
to: validatorShareAddress,
|
|
205
211
|
data: appendReferrerTracking(calldata, referrer),
|
|
206
|
-
value: 0n
|
|
212
|
+
value: 0n,
|
|
213
|
+
chainId: this.chain.id
|
|
207
214
|
}
|
|
208
215
|
}
|
|
209
216
|
}
|
|
@@ -218,8 +225,8 @@ export class PolygonStaker {
|
|
|
218
225
|
* @param params.delegatorAddress - The delegator's address
|
|
219
226
|
* @param params.validatorShareAddress - The validator's ValidatorShare contract address
|
|
220
227
|
* @param params.amount - The amount to unstake in POL (will be converted to wei internally)
|
|
221
|
-
* @param params.slippageBps -
|
|
222
|
-
* @param params.maximumSharesToBurn -
|
|
228
|
+
* @param params.slippageBps - Slippage tolerance in basis points (e.g., 50 = 0.5%). Used to calculate maximumSharesToBurn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
229
|
+
* @param params.maximumSharesToBurn - Maximum validator shares willing to burn. Exactly one of `slippageBps` or `maximumSharesToBurn` must be provided (not both, no default).
|
|
223
230
|
* @param params.referrer - (Optional) Custom referrer string for tracking. If not provided, uses 'sdk-chorusone-staking'.
|
|
224
231
|
*
|
|
225
232
|
* @returns Returns a promise that resolves to a Polygon unstaking transaction
|
|
@@ -272,7 +279,8 @@ export class PolygonStaker {
|
|
|
272
279
|
tx: {
|
|
273
280
|
to: validatorShareAddress,
|
|
274
281
|
data: appendReferrerTracking(calldata, referrer),
|
|
275
|
-
value: 0n
|
|
282
|
+
value: 0n,
|
|
283
|
+
chainId: this.chain.id
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
}
|
|
@@ -330,7 +338,8 @@ export class PolygonStaker {
|
|
|
330
338
|
tx: {
|
|
331
339
|
to: validatorShareAddress,
|
|
332
340
|
data,
|
|
333
|
-
value: 0n
|
|
341
|
+
value: 0n,
|
|
342
|
+
chainId: this.chain.id
|
|
334
343
|
}
|
|
335
344
|
}
|
|
336
345
|
}
|
|
@@ -375,7 +384,8 @@ export class PolygonStaker {
|
|
|
375
384
|
tx: {
|
|
376
385
|
to: validatorShareAddress,
|
|
377
386
|
data: appendReferrerTracking(calldata, referrer),
|
|
378
|
-
value: 0n
|
|
387
|
+
value: 0n,
|
|
388
|
+
chainId: this.chain.id
|
|
379
389
|
}
|
|
380
390
|
}
|
|
381
391
|
}
|
|
@@ -406,7 +416,10 @@ export class PolygonStaker {
|
|
|
406
416
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`)
|
|
407
417
|
}
|
|
408
418
|
|
|
409
|
-
const rewards = await
|
|
419
|
+
const [rewards] = await Promise.all([
|
|
420
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
421
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
422
|
+
])
|
|
410
423
|
if (parseEther(rewards) === 0n) {
|
|
411
424
|
throw new Error('No rewards available to compound')
|
|
412
425
|
}
|
|
@@ -420,7 +433,8 @@ export class PolygonStaker {
|
|
|
420
433
|
tx: {
|
|
421
434
|
to: validatorShareAddress,
|
|
422
435
|
data: appendReferrerTracking(calldata, referrer),
|
|
423
|
-
value: 0n
|
|
436
|
+
value: 0n,
|
|
437
|
+
chainId: this.chain.id
|
|
424
438
|
}
|
|
425
439
|
}
|
|
426
440
|
}
|
|
@@ -854,6 +868,31 @@ export class PolygonStaker {
|
|
|
854
868
|
}
|
|
855
869
|
}
|
|
856
870
|
|
|
871
|
+
private async assertValidatorActive (validatorShareAddress: Address): Promise<void> {
|
|
872
|
+
const validatorId = await this.publicClient.readContract({
|
|
873
|
+
address: validatorShareAddress,
|
|
874
|
+
abi: VALIDATOR_SHARE_ABI,
|
|
875
|
+
functionName: 'validatorId'
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
const validator = await this.publicClient.readContract({
|
|
879
|
+
address: this.contracts.stakeManagerAddress,
|
|
880
|
+
abi: STAKE_MANAGER_ABI,
|
|
881
|
+
functionName: 'validators',
|
|
882
|
+
args: [validatorId]
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
const status = Number(validator[7])
|
|
886
|
+
if (status !== 1) {
|
|
887
|
+
const statusName = VALIDATOR_STATUS[status as keyof typeof VALIDATOR_STATUS] ?? 'Unknown'
|
|
888
|
+
throw new Error(
|
|
889
|
+
`Validator is not active (status: ${statusName}). ` +
|
|
890
|
+
'The validator may be locked, inactive, or unstaked. ' +
|
|
891
|
+
'See the @chorus-one/polygon README for how to list active validators on testnet.'
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
857
896
|
private parseAmount (amount: string): bigint {
|
|
858
897
|
if (typeof amount === 'bigint') {
|
|
859
898
|
throw new Error(
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,14 @@ export interface Transaction {
|
|
|
15
15
|
data: Hex
|
|
16
16
|
/** The amount of ETH (in wei) to be sent with the transaction */
|
|
17
17
|
value?: bigint
|
|
18
|
+
/**
|
|
19
|
+
* The chain ID where the transaction must be broadcast.
|
|
20
|
+
*
|
|
21
|
+
* Polygon staking contracts live on Ethereum L1, not on the Polygon chain itself:
|
|
22
|
+
* - mainnet: `1` (Ethereum Mainnet)
|
|
23
|
+
* - testnet: `11155111` (Sepolia)
|
|
24
|
+
*/
|
|
25
|
+
chainId: number
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export interface PolygonTxStatus {
|
|
@@ -12,6 +12,7 @@ export const EXPECTED_APPROVE_TX = {
|
|
|
12
12
|
to: stakingTokenAddress as Address,
|
|
13
13
|
// approve(address spender, uint256 amount) with spender = stakeManagerAddress (0x5e3e...), amount = 100e18
|
|
14
14
|
data: '0x095ea7b30000000000000000000000005e3ef299fddf15eaa0432e6e66473ace8c13d9080000000000000000000000000000000000000000000000056bc75e2d63100000' as Hex,
|
|
15
|
-
value: 0n
|
|
15
|
+
value: 0n,
|
|
16
|
+
chainId: 1
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -93,6 +93,34 @@ describe('PolygonStaker', () => {
|
|
|
93
93
|
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
94
94
|
assert.equal(precision, EXCHANGE_RATE_HIGH_PRECISION)
|
|
95
95
|
})
|
|
96
|
+
|
|
97
|
+
it('rejects staking to an unstaked validator', async () => {
|
|
98
|
+
const unstakedValidator = '0xd245710936382e5ED099d4F8D9AAE64e67e30EF3' as Address
|
|
99
|
+
await expect(
|
|
100
|
+
staker.buildStakeTx({
|
|
101
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
102
|
+
validatorShareAddress: unstakedValidator,
|
|
103
|
+
amount: '100',
|
|
104
|
+
minSharesToMint: 0n
|
|
105
|
+
})
|
|
106
|
+
).to.be.rejectedWith('Validator is not active (status: Unstaked)')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('allows staking to an active validator', async () => {
|
|
110
|
+
// Should not throw — the default validatorShareAddress is active
|
|
111
|
+
const allowance = await staker.getAllowance(WHALE_DELEGATOR)
|
|
112
|
+
// Will fail on allowance, not on validator status
|
|
113
|
+
if (parseEther(allowance) === 0n) {
|
|
114
|
+
await expect(
|
|
115
|
+
staker.buildStakeTx({
|
|
116
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
117
|
+
validatorShareAddress,
|
|
118
|
+
amount: '100',
|
|
119
|
+
minSharesToMint: 0n
|
|
120
|
+
})
|
|
121
|
+
).to.be.rejectedWith('Insufficient POL allowance')
|
|
122
|
+
}
|
|
123
|
+
})
|
|
96
124
|
})
|
|
97
125
|
|
|
98
126
|
describe('staking lifecycle', () => {
|
package/test/lib/networks.json
CHANGED
package/test/staker.spec.ts
CHANGED
|
@@ -27,6 +27,7 @@ describe('PolygonStaker', () => {
|
|
|
27
27
|
assert.equal(tx.to, EXPECTED_APPROVE_TX.expected.to)
|
|
28
28
|
assert.equal(tx.data, EXPECTED_APPROVE_TX.expected.data)
|
|
29
29
|
assert.equal(tx.value, EXPECTED_APPROVE_TX.expected.value)
|
|
30
|
+
assert.equal(tx.chainId, EXPECTED_APPROVE_TX.expected.chainId)
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
it('should generate correct unsigned approve tx for max (unlimited) amount', async () => {
|
|
@@ -50,6 +51,22 @@ describe('PolygonStaker', () => {
|
|
|
50
51
|
})
|
|
51
52
|
})
|
|
52
53
|
|
|
54
|
+
describe('chainId', () => {
|
|
55
|
+
it('should return mainnet chainId (1) for mainnet network', async () => {
|
|
56
|
+
const { tx } = await staker.buildApproveTx({ amount: '100' })
|
|
57
|
+
assert.equal(tx.chainId, 1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should return sepolia chainId (11155111) for testnet network', async () => {
|
|
61
|
+
const testnetStaker = new PolygonStaker({
|
|
62
|
+
network: 'testnet',
|
|
63
|
+
rpcUrl: 'https://ethereum-sepolia-rpc.publicnode.com'
|
|
64
|
+
})
|
|
65
|
+
const { tx } = await testnetStaker.buildApproveTx({ amount: '100' })
|
|
66
|
+
assert.equal(tx.chainId, 11155111)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
53
70
|
describe('address validation', () => {
|
|
54
71
|
it('should reject invalid delegator address in buildStakeTx', async () => {
|
|
55
72
|
await expect(
|