@chorus-one/polygon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mocharc.json +6 -0
- package/LICENSE +13 -0
- package/README.md +233 -0
- package/dist/cjs/constants.d.ts +187 -0
- package/dist/cjs/constants.js +141 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/referrer.d.ts +2 -0
- package/dist/cjs/referrer.js +15 -0
- package/dist/cjs/staker.d.ts +335 -0
- package/dist/cjs/staker.js +716 -0
- package/dist/cjs/types.d.ts +40 -0
- package/dist/cjs/types.js +2 -0
- package/dist/mjs/constants.d.ts +187 -0
- package/dist/mjs/constants.js +138 -0
- package/dist/mjs/index.d.ts +4 -0
- package/dist/mjs/index.js +2 -0
- package/dist/mjs/package.json +3 -0
- package/dist/mjs/referrer.d.ts +2 -0
- package/dist/mjs/referrer.js +11 -0
- package/dist/mjs/staker.d.ts +335 -0
- package/dist/mjs/staker.js +712 -0
- package/dist/mjs/types.d.ts +40 -0
- package/dist/mjs/types.js +1 -0
- package/hardhat.config.ts +27 -0
- package/package.json +50 -0
- package/src/constants.ts +151 -0
- package/src/index.ts +14 -0
- package/src/referrer.ts +15 -0
- package/src/staker.ts +878 -0
- package/src/types.ts +45 -0
- package/test/fixtures/expected-data.ts +17 -0
- package/test/integration/localSigner.spec.ts +128 -0
- package/test/integration/setup.ts +41 -0
- package/test/integration/staker.spec.ts +587 -0
- package/test/integration/testStaker.ts +130 -0
- package/test/integration/utils.ts +263 -0
- package/test/lib/networks.json +14 -0
- package/test/staker.spec.ts +154 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.json +13 -0
- package/tsconfig.mjs.json +9 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { PolygonStaker, CHORUS_ONE_POLYGON_VALIDATORS } from '@chorus-one/polygon'
|
|
2
|
+
import { LocalSigner } from '@chorus-one/signer-local'
|
|
3
|
+
import { KeyType } from '@chorus-one/signer'
|
|
4
|
+
import * as bip39 from 'bip39'
|
|
5
|
+
import { HDKey } from '@scure/bip32'
|
|
6
|
+
import type { Address, PublicClient } from 'viem'
|
|
7
|
+
import { createPublicClient, http } from 'viem'
|
|
8
|
+
import { hardhat } from 'viem/chains'
|
|
9
|
+
|
|
10
|
+
export class PolygonTestStaker {
|
|
11
|
+
private mnemonic: string
|
|
12
|
+
public hdPath: string
|
|
13
|
+
public delegatorAddress: Address
|
|
14
|
+
public validatorShareAddress: Address
|
|
15
|
+
public staker: PolygonStaker
|
|
16
|
+
public localSigner: LocalSigner
|
|
17
|
+
public publicClient: PublicClient
|
|
18
|
+
|
|
19
|
+
constructor (params: { mnemonic: string; rpcUrl: string }) {
|
|
20
|
+
if (!params.mnemonic) {
|
|
21
|
+
throw new Error('Mnemonic is required')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.mnemonic = params.mnemonic
|
|
25
|
+
this.hdPath = "m/44'/60'/0'/0/0"
|
|
26
|
+
this.validatorShareAddress = CHORUS_ONE_POLYGON_VALIDATORS.mainnet
|
|
27
|
+
|
|
28
|
+
this.staker = new PolygonStaker({
|
|
29
|
+
network: 'mainnet',
|
|
30
|
+
rpcUrl: params.rpcUrl
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
this.publicClient = createPublicClient({
|
|
34
|
+
chain: hardhat,
|
|
35
|
+
transport: http(params.rpcUrl)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async init (): Promise<void> {
|
|
40
|
+
const seed = bip39.mnemonicToSeedSync(this.mnemonic)
|
|
41
|
+
const hdKey = HDKey.fromMasterSeed(seed)
|
|
42
|
+
const derived = hdKey.derive(this.hdPath)
|
|
43
|
+
const publicKey = derived.publicKey
|
|
44
|
+
|
|
45
|
+
if (!publicKey) {
|
|
46
|
+
throw new Error('Failed to derive public key')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const addressDerivationFn = PolygonStaker.getAddressDerivationFn()
|
|
50
|
+
const addresses = await addressDerivationFn(publicKey)
|
|
51
|
+
this.delegatorAddress = `0x${addresses[0]}` as Address
|
|
52
|
+
|
|
53
|
+
this.localSigner = new LocalSigner({
|
|
54
|
+
mnemonic: this.mnemonic,
|
|
55
|
+
accounts: [{ hdPath: this.hdPath }],
|
|
56
|
+
keyType: KeyType.SECP256K1,
|
|
57
|
+
addressDerivationFn
|
|
58
|
+
})
|
|
59
|
+
await this.localSigner.init()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async waitForTx (txHash: `0x${string}`): Promise<void> {
|
|
63
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash })
|
|
64
|
+
if (receipt.status !== 'success') {
|
|
65
|
+
throw new Error(`Transaction failed: ${txHash}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async approve (amount: string): Promise<string> {
|
|
70
|
+
const { tx } = await this.staker.buildApproveTx({ amount })
|
|
71
|
+
const { signedTx } = await this.staker.sign({
|
|
72
|
+
signer: this.localSigner,
|
|
73
|
+
signerAddress: this.delegatorAddress,
|
|
74
|
+
tx
|
|
75
|
+
})
|
|
76
|
+
const { txHash } = await this.staker.broadcast({ signedTx })
|
|
77
|
+
await this.waitForTx(txHash)
|
|
78
|
+
return txHash
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async stake (amount: string, minSharesToMint: bigint): Promise<string> {
|
|
82
|
+
const { tx } = await this.staker.buildStakeTx({
|
|
83
|
+
delegatorAddress: this.delegatorAddress,
|
|
84
|
+
validatorShareAddress: this.validatorShareAddress,
|
|
85
|
+
amount,
|
|
86
|
+
minSharesToMint
|
|
87
|
+
})
|
|
88
|
+
const { signedTx } = await this.staker.sign({
|
|
89
|
+
signer: this.localSigner,
|
|
90
|
+
signerAddress: this.delegatorAddress,
|
|
91
|
+
tx
|
|
92
|
+
})
|
|
93
|
+
const { txHash } = await this.staker.broadcast({ signedTx })
|
|
94
|
+
await this.waitForTx(txHash)
|
|
95
|
+
return txHash
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async unstake (amount: string, maximumSharesToBurn: bigint): Promise<string> {
|
|
99
|
+
const { tx } = await this.staker.buildUnstakeTx({
|
|
100
|
+
delegatorAddress: this.delegatorAddress,
|
|
101
|
+
validatorShareAddress: this.validatorShareAddress,
|
|
102
|
+
amount,
|
|
103
|
+
maximumSharesToBurn
|
|
104
|
+
})
|
|
105
|
+
const { signedTx } = await this.staker.sign({
|
|
106
|
+
signer: this.localSigner,
|
|
107
|
+
signerAddress: this.delegatorAddress,
|
|
108
|
+
tx
|
|
109
|
+
})
|
|
110
|
+
const { txHash } = await this.staker.broadcast({ signedTx })
|
|
111
|
+
await this.waitForTx(txHash)
|
|
112
|
+
return txHash
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async withdraw (unbondNonce: bigint): Promise<string> {
|
|
116
|
+
const { tx } = await this.staker.buildWithdrawTx({
|
|
117
|
+
delegatorAddress: this.delegatorAddress,
|
|
118
|
+
validatorShareAddress: this.validatorShareAddress,
|
|
119
|
+
unbondNonce
|
|
120
|
+
})
|
|
121
|
+
const { signedTx } = await this.staker.sign({
|
|
122
|
+
signer: this.localSigner,
|
|
123
|
+
signerAddress: this.delegatorAddress,
|
|
124
|
+
tx
|
|
125
|
+
})
|
|
126
|
+
const { txHash } = await this.staker.broadcast({ signedTx })
|
|
127
|
+
await this.waitForTx(txHash)
|
|
128
|
+
return txHash
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { PolygonStaker, CHORUS_ONE_POLYGON_VALIDATORS, NETWORK_CONTRACTS, type Transaction } from '@chorus-one/polygon'
|
|
2
|
+
import {
|
|
3
|
+
createWalletClient,
|
|
4
|
+
createPublicClient,
|
|
5
|
+
http,
|
|
6
|
+
erc20Abi,
|
|
7
|
+
encodeFunctionData,
|
|
8
|
+
parseEther,
|
|
9
|
+
toHex,
|
|
10
|
+
type PublicClient,
|
|
11
|
+
type WalletClient,
|
|
12
|
+
type Hex,
|
|
13
|
+
type Address
|
|
14
|
+
} from 'viem'
|
|
15
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
16
|
+
import { hardhat } from 'viem/chains'
|
|
17
|
+
import { assert } from 'chai'
|
|
18
|
+
import networkConfig from '../lib/networks.json'
|
|
19
|
+
|
|
20
|
+
export interface TestSetup {
|
|
21
|
+
validatorShareAddress: Address
|
|
22
|
+
walletClient: WalletClient
|
|
23
|
+
publicClient: PublicClient
|
|
24
|
+
staker: PolygonStaker
|
|
25
|
+
delegatorAddress: Address
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StakingParams {
|
|
29
|
+
delegatorAddress: Address
|
|
30
|
+
validatorShareAddress: Address
|
|
31
|
+
amount: string
|
|
32
|
+
maximumSharesToBurn?: bigint
|
|
33
|
+
staker: PolygonStaker
|
|
34
|
+
walletClient: WalletClient
|
|
35
|
+
publicClient: PublicClient
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const prepareTests = async (): Promise<TestSetup> => {
|
|
39
|
+
const privateKey = networkConfig.accounts[0].privateKey as Hex
|
|
40
|
+
const account = privateKeyToAccount(privateKey)
|
|
41
|
+
|
|
42
|
+
const walletClient = createWalletClient({
|
|
43
|
+
account,
|
|
44
|
+
chain: hardhat,
|
|
45
|
+
transport: http()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const publicClient = createPublicClient({
|
|
49
|
+
chain: hardhat,
|
|
50
|
+
transport: http()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const staker = new PolygonStaker({
|
|
54
|
+
network: 'mainnet',
|
|
55
|
+
rpcUrl: hardhat.rpcUrls.default.http[0]
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
validatorShareAddress: CHORUS_ONE_POLYGON_VALIDATORS.mainnet,
|
|
60
|
+
walletClient,
|
|
61
|
+
publicClient,
|
|
62
|
+
staker,
|
|
63
|
+
delegatorAddress: account.address
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const fundWithStakingToken = async ({
|
|
68
|
+
publicClient,
|
|
69
|
+
recipientAddress,
|
|
70
|
+
amount
|
|
71
|
+
}: {
|
|
72
|
+
publicClient: PublicClient
|
|
73
|
+
recipientAddress: Address
|
|
74
|
+
amount: bigint
|
|
75
|
+
}): Promise<void> => {
|
|
76
|
+
await impersonate({ publicClient, address: NETWORK_CONTRACTS.mainnet.stakeManagerAddress })
|
|
77
|
+
|
|
78
|
+
const impersonatedClient = createWalletClient({
|
|
79
|
+
account: NETWORK_CONTRACTS.mainnet.stakeManagerAddress,
|
|
80
|
+
chain: hardhat,
|
|
81
|
+
transport: http()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await sendTx({
|
|
85
|
+
tx: {
|
|
86
|
+
to: NETWORK_CONTRACTS.mainnet.stakingTokenAddress,
|
|
87
|
+
data: encodeFunctionData({
|
|
88
|
+
abi: erc20Abi,
|
|
89
|
+
functionName: 'transfer',
|
|
90
|
+
args: [recipientAddress, amount]
|
|
91
|
+
}),
|
|
92
|
+
value: 0n
|
|
93
|
+
},
|
|
94
|
+
walletClient: impersonatedClient,
|
|
95
|
+
publicClient,
|
|
96
|
+
senderAddress: NETWORK_CONTRACTS.mainnet.stakeManagerAddress
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const approve = async ({
|
|
101
|
+
delegatorAddress,
|
|
102
|
+
amount,
|
|
103
|
+
staker,
|
|
104
|
+
walletClient,
|
|
105
|
+
publicClient
|
|
106
|
+
}: Omit<StakingParams, 'validatorShareAddress'>): Promise<void> => {
|
|
107
|
+
const { tx } = await staker.buildApproveTx({ amount })
|
|
108
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const stake = async ({
|
|
112
|
+
delegatorAddress,
|
|
113
|
+
validatorShareAddress,
|
|
114
|
+
amount,
|
|
115
|
+
staker,
|
|
116
|
+
walletClient,
|
|
117
|
+
publicClient
|
|
118
|
+
}: StakingParams): Promise<void> => {
|
|
119
|
+
const { tx } = await staker.buildStakeTx({ delegatorAddress, validatorShareAddress, amount, minSharesToMint: 0n })
|
|
120
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const approveAndStake = async (params: StakingParams): Promise<void> => {
|
|
124
|
+
await approve(params)
|
|
125
|
+
await stake(params)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const sendTx = async ({
|
|
129
|
+
tx,
|
|
130
|
+
walletClient,
|
|
131
|
+
publicClient,
|
|
132
|
+
senderAddress
|
|
133
|
+
}: {
|
|
134
|
+
tx: Transaction
|
|
135
|
+
walletClient: WalletClient
|
|
136
|
+
publicClient: PublicClient
|
|
137
|
+
senderAddress: Address
|
|
138
|
+
}): Promise<void> => {
|
|
139
|
+
const request = await walletClient.prepareTransactionRequest({
|
|
140
|
+
...tx,
|
|
141
|
+
chain: undefined
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const hash = await walletClient.sendTransaction({
|
|
145
|
+
...request,
|
|
146
|
+
account: senderAddress
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const receipt = await publicClient.getTransactionReceipt({ hash })
|
|
150
|
+
assert.equal(receipt.status, 'success')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const unstake = async ({
|
|
154
|
+
delegatorAddress,
|
|
155
|
+
validatorShareAddress,
|
|
156
|
+
amount,
|
|
157
|
+
maximumSharesToBurn,
|
|
158
|
+
staker,
|
|
159
|
+
walletClient,
|
|
160
|
+
publicClient
|
|
161
|
+
}: Required<StakingParams>): Promise<void> => {
|
|
162
|
+
const { tx } = await staker.buildUnstakeTx({ delegatorAddress, validatorShareAddress, amount, maximumSharesToBurn })
|
|
163
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const getStakingTokenBalance = async ({
|
|
167
|
+
publicClient,
|
|
168
|
+
address
|
|
169
|
+
}: {
|
|
170
|
+
publicClient: PublicClient
|
|
171
|
+
address: Address
|
|
172
|
+
}): Promise<bigint> => {
|
|
173
|
+
return publicClient.readContract({
|
|
174
|
+
address: NETWORK_CONTRACTS.mainnet.stakingTokenAddress,
|
|
175
|
+
abi: erc20Abi,
|
|
176
|
+
functionName: 'balanceOf',
|
|
177
|
+
args: [address]
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const getWithdrawalDelay = async ({ publicClient }: { publicClient: PublicClient }): Promise<bigint> => {
|
|
182
|
+
return publicClient.readContract({
|
|
183
|
+
address: NETWORK_CONTRACTS.mainnet.stakeManagerAddress,
|
|
184
|
+
abi: [
|
|
185
|
+
{
|
|
186
|
+
type: 'function',
|
|
187
|
+
name: 'withdrawalDelay',
|
|
188
|
+
inputs: [],
|
|
189
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
190
|
+
stateMutability: 'view'
|
|
191
|
+
}
|
|
192
|
+
] as const,
|
|
193
|
+
functionName: 'withdrawalDelay'
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const impersonate = async ({
|
|
198
|
+
publicClient,
|
|
199
|
+
address
|
|
200
|
+
}: {
|
|
201
|
+
publicClient: PublicClient
|
|
202
|
+
address: Address
|
|
203
|
+
}): Promise<void> => {
|
|
204
|
+
await publicClient.request({
|
|
205
|
+
method: 'hardhat_impersonateAccount',
|
|
206
|
+
params: [address]
|
|
207
|
+
} as any)
|
|
208
|
+
|
|
209
|
+
await publicClient.request({
|
|
210
|
+
method: 'hardhat_setBalance',
|
|
211
|
+
params: [address, toHex(parseEther('10'))]
|
|
212
|
+
} as any)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Reference: https://etherscan.io/address/0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48
|
|
216
|
+
const GOVERNANCE_ADDRESS = '0x6e7a5820baD6cebA8Ef5ea69c0C92EbbDAc9CE48' as Address
|
|
217
|
+
|
|
218
|
+
// Reference: https://github.com/0xPolygon/pos-contracts/blob/main/contracts/staking/stakeManager/StakeManager.sol
|
|
219
|
+
const SET_CURRENT_EPOCH_ABI = [
|
|
220
|
+
{
|
|
221
|
+
type: 'function',
|
|
222
|
+
name: 'setCurrentEpoch',
|
|
223
|
+
inputs: [{ name: '_currentEpoch', type: 'uint256' }],
|
|
224
|
+
outputs: [],
|
|
225
|
+
stateMutability: 'nonpayable'
|
|
226
|
+
}
|
|
227
|
+
] as const
|
|
228
|
+
|
|
229
|
+
export const advanceEpoch = async ({
|
|
230
|
+
publicClient,
|
|
231
|
+
staker,
|
|
232
|
+
targetEpoch
|
|
233
|
+
}: {
|
|
234
|
+
publicClient: PublicClient
|
|
235
|
+
staker: PolygonStaker
|
|
236
|
+
targetEpoch: bigint
|
|
237
|
+
}): Promise<void> => {
|
|
238
|
+
const currentEpoch = await staker.getEpoch()
|
|
239
|
+
if (currentEpoch >= targetEpoch) return
|
|
240
|
+
|
|
241
|
+
await impersonate({ publicClient, address: GOVERNANCE_ADDRESS })
|
|
242
|
+
|
|
243
|
+
const governanceWallet = createWalletClient({
|
|
244
|
+
account: GOVERNANCE_ADDRESS,
|
|
245
|
+
chain: hardhat,
|
|
246
|
+
transport: http()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await sendTx({
|
|
250
|
+
tx: {
|
|
251
|
+
to: NETWORK_CONTRACTS.mainnet.stakeManagerAddress,
|
|
252
|
+
data: encodeFunctionData({
|
|
253
|
+
abi: SET_CURRENT_EPOCH_ABI,
|
|
254
|
+
functionName: 'setCurrentEpoch',
|
|
255
|
+
args: [targetEpoch]
|
|
256
|
+
}),
|
|
257
|
+
value: 0n
|
|
258
|
+
},
|
|
259
|
+
walletClient: governanceWallet,
|
|
260
|
+
publicClient,
|
|
261
|
+
senderAddress: GOVERNANCE_ADDRESS
|
|
262
|
+
})
|
|
263
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"networks": {
|
|
3
|
+
"ethereum": {
|
|
4
|
+
"name": "ethereum",
|
|
5
|
+
"url": "https://ethereum-rpc.publicnode.com"
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"accounts": [
|
|
9
|
+
{
|
|
10
|
+
"privateKey": "0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd",
|
|
11
|
+
"balance": "10000000000000000000000"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { PolygonStaker } from '../src/staker'
|
|
2
|
+
import { NETWORK_CONTRACTS } from '../src/constants'
|
|
3
|
+
import { describe, it, beforeEach } from 'mocha'
|
|
4
|
+
import { use, expect, assert } from 'chai'
|
|
5
|
+
import chaiAsPromised from 'chai-as-promised'
|
|
6
|
+
import { type Address, maxUint256, encodeFunctionData, erc20Abi } from 'viem'
|
|
7
|
+
import { EXPECTED_APPROVE_TX, TEST_ADDRESS, TEST_VALIDATOR_SHARE } from './fixtures/expected-data'
|
|
8
|
+
|
|
9
|
+
use(chaiAsPromised)
|
|
10
|
+
|
|
11
|
+
describe('PolygonStaker', () => {
|
|
12
|
+
let staker: PolygonStaker
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
staker = new PolygonStaker({
|
|
16
|
+
network: 'mainnet',
|
|
17
|
+
rpcUrl: 'https://ethereum-rpc.publicnode.com'
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('buildApproveTx', () => {
|
|
22
|
+
it('should generate correct unsigned approve tx', async () => {
|
|
23
|
+
const { tx } = await staker.buildApproveTx({
|
|
24
|
+
amount: EXPECTED_APPROVE_TX.amount
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
assert.equal(tx.to, EXPECTED_APPROVE_TX.expected.to)
|
|
28
|
+
assert.equal(tx.data, EXPECTED_APPROVE_TX.expected.data)
|
|
29
|
+
assert.equal(tx.value, EXPECTED_APPROVE_TX.expected.value)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should generate correct unsigned approve tx for max (unlimited) amount', async () => {
|
|
33
|
+
const { tx } = await staker.buildApproveTx({ amount: 'max' })
|
|
34
|
+
|
|
35
|
+
const expectedData = encodeFunctionData({
|
|
36
|
+
abi: erc20Abi,
|
|
37
|
+
functionName: 'approve',
|
|
38
|
+
args: [NETWORK_CONTRACTS.mainnet.stakeManagerAddress, maxUint256]
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
assert.equal(tx.to, EXPECTED_APPROVE_TX.expected.to)
|
|
42
|
+
assert.equal(tx.data, expectedData)
|
|
43
|
+
assert.equal(tx.value, EXPECTED_APPROVE_TX.expected.value)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should reject invalid amounts', async () => {
|
|
47
|
+
await expect(staker.buildApproveTx({ amount: '0' })).to.be.rejectedWith('Amount must be greater than 0')
|
|
48
|
+
await expect(staker.buildApproveTx({ amount: '' })).to.be.rejectedWith('Amount cannot be empty')
|
|
49
|
+
await expect(staker.buildApproveTx({ amount: 'invalid' })).to.be.rejectedWith('Amount must be a valid number')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('address validation', () => {
|
|
54
|
+
it('should reject invalid delegator address in buildStakeTx', async () => {
|
|
55
|
+
await expect(
|
|
56
|
+
staker.buildStakeTx({
|
|
57
|
+
delegatorAddress: 'invalid' as Address,
|
|
58
|
+
validatorShareAddress: TEST_VALIDATOR_SHARE,
|
|
59
|
+
amount: '100',
|
|
60
|
+
minSharesToMint: 0n
|
|
61
|
+
})
|
|
62
|
+
).to.be.rejectedWith('Invalid delegator address')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should reject invalid validator share address in buildStakeTx', async () => {
|
|
66
|
+
await expect(
|
|
67
|
+
staker.buildStakeTx({
|
|
68
|
+
delegatorAddress: TEST_ADDRESS,
|
|
69
|
+
validatorShareAddress: 'invalid' as Address,
|
|
70
|
+
amount: '100',
|
|
71
|
+
minSharesToMint: 0n
|
|
72
|
+
})
|
|
73
|
+
).to.be.rejectedWith('Invalid validator share address')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should reject invalid delegator address in buildUnstakeTx', async () => {
|
|
77
|
+
await expect(
|
|
78
|
+
staker.buildUnstakeTx({
|
|
79
|
+
delegatorAddress: 'invalid' as Address,
|
|
80
|
+
validatorShareAddress: TEST_VALIDATOR_SHARE,
|
|
81
|
+
amount: '100',
|
|
82
|
+
maximumSharesToBurn: 0n
|
|
83
|
+
})
|
|
84
|
+
).to.be.rejectedWith('Invalid delegator address')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should reject invalid validator share address in buildUnstakeTx', async () => {
|
|
88
|
+
await expect(
|
|
89
|
+
staker.buildUnstakeTx({
|
|
90
|
+
delegatorAddress: TEST_ADDRESS,
|
|
91
|
+
validatorShareAddress: 'invalid' as Address,
|
|
92
|
+
amount: '100',
|
|
93
|
+
maximumSharesToBurn: 0n
|
|
94
|
+
})
|
|
95
|
+
).to.be.rejectedWith('Invalid validator share address')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('buildStakeTx slippage validation', () => {
|
|
100
|
+
it('should reject when both slippageBps and minSharesToMint are provided', async () => {
|
|
101
|
+
await expect(
|
|
102
|
+
staker.buildStakeTx({
|
|
103
|
+
delegatorAddress: TEST_ADDRESS,
|
|
104
|
+
validatorShareAddress: TEST_VALIDATOR_SHARE,
|
|
105
|
+
amount: '100',
|
|
106
|
+
slippageBps: 50,
|
|
107
|
+
minSharesToMint: 100n
|
|
108
|
+
})
|
|
109
|
+
).to.be.rejectedWith('Cannot specify both slippageBps and minSharesToMint. Use one or the other.')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('buildUnstakeTx slippage validation', () => {
|
|
114
|
+
it('should reject when both slippageBps and maximumSharesToBurn are provided', async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
staker.buildUnstakeTx({
|
|
117
|
+
delegatorAddress: TEST_ADDRESS,
|
|
118
|
+
validatorShareAddress: TEST_VALIDATOR_SHARE,
|
|
119
|
+
amount: '100',
|
|
120
|
+
slippageBps: 50,
|
|
121
|
+
maximumSharesToBurn: 100n
|
|
122
|
+
})
|
|
123
|
+
).to.be.rejectedWith('Cannot specify both slippageBps and maximumSharesToBurn. Use one or the other.')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('getUnbonds', () => {
|
|
128
|
+
it('should return empty array for empty nonces input', async () => {
|
|
129
|
+
const result = await staker.getUnbonds({
|
|
130
|
+
delegatorAddress: TEST_ADDRESS,
|
|
131
|
+
validatorShareAddress: TEST_VALIDATOR_SHARE,
|
|
132
|
+
unbondNonces: []
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
assert.isArray(result)
|
|
136
|
+
assert.lengthOf(result, 0)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('getAddressDerivationFn', () => {
|
|
141
|
+
it('should derive correct ethereum address from a compressed public key', async () => {
|
|
142
|
+
const fn = PolygonStaker.getAddressDerivationFn()
|
|
143
|
+
|
|
144
|
+
// Known secp256k1 compressed public key (generator point)
|
|
145
|
+
const compressedPubKey = Uint8Array.from(
|
|
146
|
+
Buffer.from('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'hex')
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const [address] = await fn(compressedPubKey)
|
|
150
|
+
assert.isString(address)
|
|
151
|
+
assert.equal(address.length, 40)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@chorus-one/signer": ["../signer/src"]
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"include": ["src"],
|
|
11
|
+
"exclude": ["node_modules", "dist"],
|
|
12
|
+
"references": [{ "path": "../signer" }]
|
|
13
|
+
}
|