@chorus-one/polygon 1.0.3 → 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 +14 -1
- package/dist/cjs/constants.d.ts +55 -0
- package/dist/cjs/constants.js +32 -3
- package/dist/cjs/staker.d.ts +1 -0
- package/dist/cjs/staker.js +40 -8
- 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 +1 -0
- package/dist/mjs/staker.js +41 -9
- package/dist/mjs/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/constants.ts +32 -2
- package/src/staker.ts +46 -8
- package/src/types.ts +8 -0
- package/test/fixtures/expected-data.ts +2 -1
- package/test/integration/staker.spec.ts +28 -0
- package/test/staker.spec.ts +17 -0
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ console.log(status) // 'success', 'failure', or 'unknown'
|
|
|
216
216
|
|
|
217
217
|
## Key Features
|
|
218
218
|
|
|
219
|
-
- **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).
|
|
220
220
|
- **POL Token Staking**: Stake the native POL token (formerly MATIC) to validators
|
|
221
221
|
- **Human-Readable Amounts**: Pass token amounts as strings (e.g., '1.5'), conversion to wei is handled automatically
|
|
222
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.
|
|
@@ -231,6 +231,19 @@ console.log(status) // 'success', 'failure', or 'unknown'
|
|
|
231
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.
|
|
232
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.
|
|
233
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
|
+
```
|
|
234
247
|
|
|
235
248
|
## License
|
|
236
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
package/dist/cjs/staker.js
CHANGED
|
@@ -95,7 +95,8 @@ class PolygonStaker {
|
|
|
95
95
|
tx: {
|
|
96
96
|
to: this.contracts.stakingTokenAddress,
|
|
97
97
|
data,
|
|
98
|
-
value: 0n
|
|
98
|
+
value: 0n,
|
|
99
|
+
chainId: this.chain.id
|
|
99
100
|
}
|
|
100
101
|
};
|
|
101
102
|
}
|
|
@@ -128,7 +129,10 @@ class PolygonStaker {
|
|
|
128
129
|
throw new Error('Cannot specify both slippageBps and minSharesToMint. Use one or the other.');
|
|
129
130
|
}
|
|
130
131
|
const amountWei = this.parseAmount(amount);
|
|
131
|
-
const allowance = await
|
|
132
|
+
const [allowance] = await Promise.all([
|
|
133
|
+
this.getAllowance(delegatorAddress),
|
|
134
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
135
|
+
]);
|
|
132
136
|
if ((0, viem_1.parseEther)(allowance) < amountWei) {
|
|
133
137
|
throw new Error(`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`);
|
|
134
138
|
}
|
|
@@ -152,7 +156,8 @@ class PolygonStaker {
|
|
|
152
156
|
tx: {
|
|
153
157
|
to: validatorShareAddress,
|
|
154
158
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
155
|
-
value: 0n
|
|
159
|
+
value: 0n,
|
|
160
|
+
chainId: this.chain.id
|
|
156
161
|
}
|
|
157
162
|
};
|
|
158
163
|
}
|
|
@@ -206,7 +211,8 @@ class PolygonStaker {
|
|
|
206
211
|
tx: {
|
|
207
212
|
to: validatorShareAddress,
|
|
208
213
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
209
|
-
value: 0n
|
|
214
|
+
value: 0n,
|
|
215
|
+
chainId: this.chain.id
|
|
210
216
|
}
|
|
211
217
|
};
|
|
212
218
|
}
|
|
@@ -253,7 +259,8 @@ class PolygonStaker {
|
|
|
253
259
|
tx: {
|
|
254
260
|
to: validatorShareAddress,
|
|
255
261
|
data,
|
|
256
|
-
value: 0n
|
|
262
|
+
value: 0n,
|
|
263
|
+
chainId: this.chain.id
|
|
257
264
|
}
|
|
258
265
|
};
|
|
259
266
|
}
|
|
@@ -289,7 +296,8 @@ class PolygonStaker {
|
|
|
289
296
|
tx: {
|
|
290
297
|
to: validatorShareAddress,
|
|
291
298
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
292
|
-
value: 0n
|
|
299
|
+
value: 0n,
|
|
300
|
+
chainId: this.chain.id
|
|
293
301
|
}
|
|
294
302
|
};
|
|
295
303
|
}
|
|
@@ -313,7 +321,10 @@ class PolygonStaker {
|
|
|
313
321
|
if (!(0, viem_1.isAddress)(validatorShareAddress)) {
|
|
314
322
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`);
|
|
315
323
|
}
|
|
316
|
-
const rewards = await
|
|
324
|
+
const [rewards] = await Promise.all([
|
|
325
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
326
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
327
|
+
]);
|
|
317
328
|
if ((0, viem_1.parseEther)(rewards) === 0n) {
|
|
318
329
|
throw new Error('No rewards available to compound');
|
|
319
330
|
}
|
|
@@ -325,7 +336,8 @@ class PolygonStaker {
|
|
|
325
336
|
tx: {
|
|
326
337
|
to: validatorShareAddress,
|
|
327
338
|
data: (0, referrer_1.appendReferrerTracking)(calldata, referrer),
|
|
328
|
-
value: 0n
|
|
339
|
+
value: 0n,
|
|
340
|
+
chainId: this.chain.id
|
|
329
341
|
}
|
|
330
342
|
};
|
|
331
343
|
}
|
|
@@ -693,6 +705,26 @@ class PolygonStaker {
|
|
|
693
705
|
};
|
|
694
706
|
}
|
|
695
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
|
+
}
|
|
696
728
|
parseAmount(amount) {
|
|
697
729
|
if (typeof amount === 'bigint') {
|
|
698
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
package/dist/mjs/staker.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createPublicClient, http, encodeFunctionData, parseEther, formatEther,
|
|
|
2
2
|
import { mainnet, sepolia } from 'viem/chains';
|
|
3
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
|
*
|
|
@@ -92,7 +92,8 @@ export class PolygonStaker {
|
|
|
92
92
|
tx: {
|
|
93
93
|
to: this.contracts.stakingTokenAddress,
|
|
94
94
|
data,
|
|
95
|
-
value: 0n
|
|
95
|
+
value: 0n,
|
|
96
|
+
chainId: this.chain.id
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
99
|
}
|
|
@@ -125,7 +126,10 @@ export class PolygonStaker {
|
|
|
125
126
|
throw new Error('Cannot specify both slippageBps and minSharesToMint. Use one or the other.');
|
|
126
127
|
}
|
|
127
128
|
const amountWei = this.parseAmount(amount);
|
|
128
|
-
const allowance = await
|
|
129
|
+
const [allowance] = await Promise.all([
|
|
130
|
+
this.getAllowance(delegatorAddress),
|
|
131
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
132
|
+
]);
|
|
129
133
|
if (parseEther(allowance) < amountWei) {
|
|
130
134
|
throw new Error(`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`);
|
|
131
135
|
}
|
|
@@ -149,7 +153,8 @@ export class PolygonStaker {
|
|
|
149
153
|
tx: {
|
|
150
154
|
to: validatorShareAddress,
|
|
151
155
|
data: appendReferrerTracking(calldata, referrer),
|
|
152
|
-
value: 0n
|
|
156
|
+
value: 0n,
|
|
157
|
+
chainId: this.chain.id
|
|
153
158
|
}
|
|
154
159
|
};
|
|
155
160
|
}
|
|
@@ -203,7 +208,8 @@ export class PolygonStaker {
|
|
|
203
208
|
tx: {
|
|
204
209
|
to: validatorShareAddress,
|
|
205
210
|
data: appendReferrerTracking(calldata, referrer),
|
|
206
|
-
value: 0n
|
|
211
|
+
value: 0n,
|
|
212
|
+
chainId: this.chain.id
|
|
207
213
|
}
|
|
208
214
|
};
|
|
209
215
|
}
|
|
@@ -250,7 +256,8 @@ export class PolygonStaker {
|
|
|
250
256
|
tx: {
|
|
251
257
|
to: validatorShareAddress,
|
|
252
258
|
data,
|
|
253
|
-
value: 0n
|
|
259
|
+
value: 0n,
|
|
260
|
+
chainId: this.chain.id
|
|
254
261
|
}
|
|
255
262
|
};
|
|
256
263
|
}
|
|
@@ -286,7 +293,8 @@ export class PolygonStaker {
|
|
|
286
293
|
tx: {
|
|
287
294
|
to: validatorShareAddress,
|
|
288
295
|
data: appendReferrerTracking(calldata, referrer),
|
|
289
|
-
value: 0n
|
|
296
|
+
value: 0n,
|
|
297
|
+
chainId: this.chain.id
|
|
290
298
|
}
|
|
291
299
|
};
|
|
292
300
|
}
|
|
@@ -310,7 +318,10 @@ export class PolygonStaker {
|
|
|
310
318
|
if (!isAddress(validatorShareAddress)) {
|
|
311
319
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`);
|
|
312
320
|
}
|
|
313
|
-
const rewards = await
|
|
321
|
+
const [rewards] = await Promise.all([
|
|
322
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
323
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
324
|
+
]);
|
|
314
325
|
if (parseEther(rewards) === 0n) {
|
|
315
326
|
throw new Error('No rewards available to compound');
|
|
316
327
|
}
|
|
@@ -322,7 +333,8 @@ export class PolygonStaker {
|
|
|
322
333
|
tx: {
|
|
323
334
|
to: validatorShareAddress,
|
|
324
335
|
data: appendReferrerTracking(calldata, referrer),
|
|
325
|
-
value: 0n
|
|
336
|
+
value: 0n,
|
|
337
|
+
chainId: this.chain.id
|
|
326
338
|
}
|
|
327
339
|
};
|
|
328
340
|
}
|
|
@@ -690,6 +702,26 @@ export class PolygonStaker {
|
|
|
690
702
|
};
|
|
691
703
|
}
|
|
692
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
|
+
}
|
|
693
725
|
parseAmount(amount) {
|
|
694
726
|
if (typeof amount === 'bigint') {
|
|
695
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",
|
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
|
@@ -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'
|
|
@@ -130,7 +131,8 @@ export class PolygonStaker {
|
|
|
130
131
|
tx: {
|
|
131
132
|
to: this.contracts.stakingTokenAddress,
|
|
132
133
|
data,
|
|
133
|
-
value: 0n
|
|
134
|
+
value: 0n,
|
|
135
|
+
chainId: this.chain.id
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
}
|
|
@@ -174,7 +176,10 @@ export class PolygonStaker {
|
|
|
174
176
|
|
|
175
177
|
const amountWei = this.parseAmount(amount)
|
|
176
178
|
|
|
177
|
-
const allowance = await
|
|
179
|
+
const [allowance] = await Promise.all([
|
|
180
|
+
this.getAllowance(delegatorAddress),
|
|
181
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
182
|
+
])
|
|
178
183
|
if (parseEther(allowance) < amountWei) {
|
|
179
184
|
throw new Error(
|
|
180
185
|
`Insufficient POL allowance. Current: ${allowance}, Required: ${amount}. Call buildApproveTx() first.`
|
|
@@ -204,7 +209,8 @@ export class PolygonStaker {
|
|
|
204
209
|
tx: {
|
|
205
210
|
to: validatorShareAddress,
|
|
206
211
|
data: appendReferrerTracking(calldata, referrer),
|
|
207
|
-
value: 0n
|
|
212
|
+
value: 0n,
|
|
213
|
+
chainId: this.chain.id
|
|
208
214
|
}
|
|
209
215
|
}
|
|
210
216
|
}
|
|
@@ -273,7 +279,8 @@ export class PolygonStaker {
|
|
|
273
279
|
tx: {
|
|
274
280
|
to: validatorShareAddress,
|
|
275
281
|
data: appendReferrerTracking(calldata, referrer),
|
|
276
|
-
value: 0n
|
|
282
|
+
value: 0n,
|
|
283
|
+
chainId: this.chain.id
|
|
277
284
|
}
|
|
278
285
|
}
|
|
279
286
|
}
|
|
@@ -331,7 +338,8 @@ export class PolygonStaker {
|
|
|
331
338
|
tx: {
|
|
332
339
|
to: validatorShareAddress,
|
|
333
340
|
data,
|
|
334
|
-
value: 0n
|
|
341
|
+
value: 0n,
|
|
342
|
+
chainId: this.chain.id
|
|
335
343
|
}
|
|
336
344
|
}
|
|
337
345
|
}
|
|
@@ -376,7 +384,8 @@ export class PolygonStaker {
|
|
|
376
384
|
tx: {
|
|
377
385
|
to: validatorShareAddress,
|
|
378
386
|
data: appendReferrerTracking(calldata, referrer),
|
|
379
|
-
value: 0n
|
|
387
|
+
value: 0n,
|
|
388
|
+
chainId: this.chain.id
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
}
|
|
@@ -407,7 +416,10 @@ export class PolygonStaker {
|
|
|
407
416
|
throw new Error(`Invalid validator share address: ${validatorShareAddress}`)
|
|
408
417
|
}
|
|
409
418
|
|
|
410
|
-
const rewards = await
|
|
419
|
+
const [rewards] = await Promise.all([
|
|
420
|
+
this.getLiquidRewards({ delegatorAddress, validatorShareAddress }),
|
|
421
|
+
this.assertValidatorActive(validatorShareAddress)
|
|
422
|
+
])
|
|
411
423
|
if (parseEther(rewards) === 0n) {
|
|
412
424
|
throw new Error('No rewards available to compound')
|
|
413
425
|
}
|
|
@@ -421,7 +433,8 @@ export class PolygonStaker {
|
|
|
421
433
|
tx: {
|
|
422
434
|
to: validatorShareAddress,
|
|
423
435
|
data: appendReferrerTracking(calldata, referrer),
|
|
424
|
-
value: 0n
|
|
436
|
+
value: 0n,
|
|
437
|
+
chainId: this.chain.id
|
|
425
438
|
}
|
|
426
439
|
}
|
|
427
440
|
}
|
|
@@ -855,6 +868,31 @@ export class PolygonStaker {
|
|
|
855
868
|
}
|
|
856
869
|
}
|
|
857
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
|
+
|
|
858
896
|
private parseAmount (amount: string): bigint {
|
|
859
897
|
if (typeof amount === 'bigint') {
|
|
860
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/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(
|