Skip to content

Commit c52414c

Browse files
authored
Add pallet staking migration from Currency to Fungible trait (#66)
1 parent 475313a commit c52414c

File tree

3 files changed

+705
-0
lines changed

3 files changed

+705
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/*
2+
Query and export accounts that have staking locks (not yet migrated to freezes)
3+
4+
This helper script queries the chain for accounts with staking locks (stkngcol, stkngdel)
5+
that haven't been migrated to the new freeze system yet. It exports the results to a JSON file
6+
that can be used as input for the migration script.
7+
8+
Context: PR https://github.com/moonbeam-foundation/moonbeam/pull/3306
9+
10+
Ex: npx tsx src/lazy-migrations/006-get-accounts-with-staking-locks.ts --url ws://localhost:9944
11+
*/
12+
13+
import yargs from "yargs";
14+
import * as path from "path";
15+
import { ApiPromise, WsProvider } from "@polkadot/api";
16+
import { NETWORK_YARGS_OPTIONS } from "../utils/networks.ts";
17+
import * as fs from "fs";
18+
19+
const argv = yargs(process.argv.slice(2))
20+
.usage("Usage: $0")
21+
.version("1.0.0")
22+
.options({
23+
...NETWORK_YARGS_OPTIONS,
24+
"output-file": {
25+
type: "string",
26+
demandOption: false,
27+
describe: "Path to output JSON file (optional, auto-generated if not provided)",
28+
},
29+
}).argv;
30+
31+
interface AccountInfo {
32+
address: string;
33+
locks: {
34+
id: string;
35+
amount: string;
36+
}[];
37+
freezes: {
38+
id: string;
39+
amount: string;
40+
}[];
41+
}
42+
43+
interface OutputData {
44+
delegators: string[];
45+
candidates: string[];
46+
}
47+
48+
async function main() {
49+
// Create provider with extended timeout for large state queries
50+
// WsProvider(endpoint, autoConnectMs, headers, timeout, cacheCapacity, cacheTtl)
51+
const wsProvider = new WsProvider(
52+
argv.url || process.env.MOONBEAM_TOOLS_WS_URL,
53+
undefined, // autoConnectMs - use default auto-reconnect
54+
undefined, // headers
55+
300000, // timeout - 5 minute timeout for large queries
56+
);
57+
const api = await ApiPromise.create({
58+
noInitWarn: true,
59+
provider: wsProvider,
60+
});
61+
62+
console.log("Connected to chain");
63+
64+
const chain = (await api.rpc.system.chain()).toString().toLowerCase().replace(/\s/g, "-");
65+
const OUTPUT_FILE =
66+
argv["output-file"] ||
67+
path.resolve(process.cwd(), `src/lazy-migrations/accounts-with-staking-locks--${chain}.json`);
68+
const DETAILED_OUTPUT_FILE =
69+
argv["output-file"] ||
70+
path.resolve(
71+
process.cwd(),
72+
`src/lazy-migrations/accounts-with-staking-locks--${chain}.info.json`,
73+
);
74+
75+
try {
76+
console.log("Querying delegator and collator state from parachain staking...");
77+
console.log("Using pagination to efficiently handle large datasets...\n");
78+
79+
const delegatorAddresses: string[] = [];
80+
const candidateAddresses: string[] = [];
81+
const detailedInfo: AccountInfo[] = [];
82+
const PAGE_SIZE = 50; // Balance between efficiency and RPC timeout risk
83+
84+
// Get all delegators from parachain staking with pagination
85+
console.log("Querying delegators with pagination...");
86+
let delegatorCount = 0;
87+
let lastDelegatorKey: any = null;
88+
89+
for (;;) {
90+
const delegatorBatch = await api.query.parachainStaking.delegatorState.entriesPaged({
91+
args: [],
92+
pageSize: PAGE_SIZE,
93+
startKey: lastDelegatorKey,
94+
});
95+
96+
console.log(
97+
` Loaded ${delegatorBatch.length} delegators (batch ${Math.floor(delegatorCount / PAGE_SIZE) + 1})...`,
98+
);
99+
100+
for (const [key, value] of delegatorBatch) {
101+
if (value.isEmpty) continue;
102+
103+
// Extract account ID from storage key using args
104+
const accountId = key.args[0];
105+
const accountIdStr = accountId.toString();
106+
107+
// Check if this delegator has been migrated
108+
const isMigrated = await api.query.parachainStaking.migratedDelegators(accountId);
109+
110+
if (!isMigrated.toHuman()) {
111+
// Not migrated yet, get lock and freeze info
112+
const locks = await api.query.balances.locks(accountId);
113+
const freezes = await api.query.balances.freezes(accountId);
114+
115+
const lockData = locks.toJSON() as any[];
116+
const freezeData = freezes.toJSON() as any[];
117+
118+
const stakingLocks = lockData.filter(
119+
(lock: any) => lock.id === "stkngdel" || lock.id === "0x73746b6e6764656c",
120+
);
121+
122+
if (stakingLocks.length > 0) {
123+
delegatorAddresses.push(accountIdStr);
124+
detailedInfo.push({
125+
address: accountIdStr,
126+
locks: stakingLocks,
127+
freezes: freezeData,
128+
});
129+
}
130+
}
131+
132+
delegatorCount++;
133+
lastDelegatorKey = key;
134+
}
135+
136+
// Save progress after each batch
137+
const outputData: OutputData = {
138+
delegators: delegatorAddresses,
139+
candidates: candidateAddresses,
140+
};
141+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(outputData, null, 2));
142+
fs.writeFileSync(DETAILED_OUTPUT_FILE, JSON.stringify(detailedInfo, null, 2));
143+
console.log(
144+
` Progress saved: ${delegatorAddresses.length} delegators, ${candidateAddresses.length} candidates`,
145+
);
146+
147+
if (delegatorBatch.length < PAGE_SIZE) {
148+
console.log(`\nTotal delegators processed: ${delegatorCount}`);
149+
break;
150+
}
151+
}
152+
153+
console.log(`Delegators needing migration: ${delegatorAddresses.length}\n`);
154+
155+
// Get all collator candidates with pagination
156+
console.log("Querying candidates with pagination...");
157+
let candidateCount = 0;
158+
let lastCandidateKey: any = null;
159+
160+
for (;;) {
161+
const candidateBatch = await api.query.parachainStaking.candidateInfo.entriesPaged({
162+
args: [],
163+
pageSize: PAGE_SIZE,
164+
startKey: lastCandidateKey,
165+
});
166+
167+
console.log(
168+
` Loaded ${candidateBatch.length} candidates (batch ${Math.floor(candidateCount / PAGE_SIZE) + 1})...`,
169+
);
170+
171+
for (const [key, value] of candidateBatch) {
172+
if (value.isEmpty) continue;
173+
174+
// Extract account ID from storage key using args
175+
const accountId = key.args[0];
176+
const accountIdStr = accountId.toString();
177+
178+
// Check if this candidate has been migrated
179+
const isMigrated = await api.query.parachainStaking.migratedCandidates(accountId);
180+
181+
if (!isMigrated.toHuman()) {
182+
// Not migrated yet, get lock and freeze info
183+
const locks = await api.query.balances.locks(accountId);
184+
const freezes = await api.query.balances.freezes(accountId);
185+
186+
const lockData = locks.toJSON() as any[];
187+
const freezeData = freezes.toJSON() as any[];
188+
189+
const stakingLocks = lockData.filter(
190+
(lock: any) => lock.id === "stkngcol" || lock.id === "0x73746b6e67636f6c",
191+
);
192+
193+
if (stakingLocks.length > 0) {
194+
candidateAddresses.push(accountIdStr);
195+
detailedInfo.push({
196+
address: accountIdStr,
197+
locks: stakingLocks,
198+
freezes: freezeData,
199+
});
200+
}
201+
}
202+
203+
candidateCount++;
204+
lastCandidateKey = key;
205+
}
206+
207+
// Save progress after each batch
208+
const outputData: OutputData = {
209+
delegators: delegatorAddresses,
210+
candidates: candidateAddresses,
211+
};
212+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(outputData, null, 2));
213+
fs.writeFileSync(DETAILED_OUTPUT_FILE, JSON.stringify(detailedInfo, null, 2));
214+
console.log(
215+
` Progress saved: ${delegatorAddresses.length} delegators, ${candidateAddresses.length} candidates`,
216+
);
217+
218+
if (candidateBatch.length < PAGE_SIZE) {
219+
console.log(`\nTotal candidates processed: ${candidateCount}`);
220+
break;
221+
}
222+
}
223+
224+
console.log(`Candidates needing migration: ${candidateAddresses.length}\n`);
225+
226+
const totalAccounts = delegatorAddresses.length + candidateAddresses.length;
227+
console.log(`\n\nFound ${totalAccounts} accounts that need migration`);
228+
229+
// Save the categorized lists of addresses
230+
const outputData: OutputData = {
231+
delegators: delegatorAddresses,
232+
candidates: candidateAddresses,
233+
};
234+
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(outputData, null, 2));
235+
console.log(`\nSaved account addresses to: ${OUTPUT_FILE}`);
236+
237+
// Save detailed information
238+
fs.writeFileSync(DETAILED_OUTPUT_FILE, JSON.stringify(detailedInfo, null, 2));
239+
console.log(`Saved detailed information to: ${DETAILED_OUTPUT_FILE}`);
240+
241+
// Print summary
242+
console.log("\n" + "=".repeat(50));
243+
console.log("Summary:");
244+
console.log("=".repeat(50));
245+
console.log(`Total delegators checked: ${delegatorCount}`);
246+
console.log(`Total candidates checked: ${candidateCount}`);
247+
console.log(`Accounts needing migration: ${totalAccounts}`);
248+
console.log(` - Delegators: ${delegatorAddresses.length}`);
249+
console.log(` - Candidates: ${candidateAddresses.length}`);
250+
251+
// Print first few examples
252+
if (delegatorAddresses.length > 0) {
253+
console.log("\nFirst 5 delegators to migrate:");
254+
delegatorAddresses.slice(0, 5).forEach((addr, idx) => {
255+
console.log(` ${idx + 1}. ${addr}`);
256+
});
257+
}
258+
259+
if (candidateAddresses.length > 0) {
260+
console.log("\nFirst 5 candidates to migrate:");
261+
candidateAddresses.slice(0, 5).forEach((addr, idx) => {
262+
console.log(` ${idx + 1}. ${addr}`);
263+
});
264+
}
265+
} catch (error) {
266+
console.error("Query error:", error);
267+
throw error;
268+
} finally {
269+
await api.disconnect();
270+
}
271+
}
272+
273+
main().catch((err) => {
274+
console.error("ERR!", err);
275+
process.exit(1);
276+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Migrate Locks to Freezes - Migration Script
2+
3+
This migration facilitates the transition from the deprecated `Currency` trait with lock-based fund restrictions to the modern `Fungible` trait with freeze-based mechanisms for parachain staking.
4+
5+
## Context
6+
7+
Related to [PR #3306](https://github.com/moonbeam-foundation/moonbeam/pull/3306), this migration converts staking locks (`stkngcol`, `stkngdel`) to staking freezes (`StakingCollator`, `StakingDelegator`).
8+
9+
### What Changed
10+
11+
- **Before**: Accounts had staking locks visible in `Balances.Locks` with identifiers `stkngcol` (collators) and `stkngdel` (delegators)
12+
- **After**: Accounts have staking freezes visible in `Balances.Freezes` with reasons `StakingCollator` and `StakingDelegator`
13+
14+
### Migration Mechanism
15+
16+
The migration is lazy - accounts automatically migrate during their next staking interaction. However, this script allows proactive batch migration of accounts before they interact with the staking pallet.
17+
18+
## Scripts
19+
20+
### 1. Query Accounts (Helper Script)
21+
22+
**File**: `006-get-accounts-with-staking-locks.ts`
23+
24+
Queries the chain for accounts that still have staking locks and haven't been migrated to freezes yet.
25+
26+
#### Usage
27+
28+
```bash
29+
npx tsx src/lazy-migrations/006-get-accounts-with-staking-locks.ts --url wss://wss.api.moonbeam.network
30+
```
31+
32+
#### Options
33+
34+
- `--url`: WebSocket URL of the chain
35+
- `--output-file`: (Optional) Custom path for output JSON file
36+
37+
#### Output
38+
39+
Creates two files:
40+
41+
1. `accounts-with-staking-locks--{chain}.json` - Simple array of account addresses
42+
2. `accounts-with-staking-locks--{chain}.info.json` - Detailed information with locks and freezes
43+
44+
### 2. Migration Script
45+
46+
**File**: `006-migrate-locks-to-freezes.ts`
47+
48+
Performs the actual migration by calling `parachainStaking.migrateLocksToFreezesBatch` with batches of up to 100 accounts.
49+
50+
#### Usage
51+
52+
```bash
53+
# Using pre-generated account list
54+
bun src/lazy-migrations/006-migrate-locks-to-freezes.ts \
55+
--url wss://wss.api.moonbeam.network \
56+
--account-priv-key <private-key> \
57+
--input-file src/lazy-migrations/accounts-with-staking-locks--moonbeam.json \
58+
--limit 50
59+
```
60+
61+
#### Options
62+
63+
- `--url`: WebSocket URL of the chain
64+
- `--account-priv-key`: Private key of the account to sign transactions (required unless using `--alith`)
65+
- `--alith`: Use Alith's private key (dev/testing only)
66+
- `--limit`: Maximum number of accounts per batch (default: 100, max: 100)
67+
- `--input-file`: (Optional) Path to JSON file with account addresses. If not provided, will query chain state
68+
69+
#### Progress Tracking
70+
71+
The script creates a progress file: `locks-to-freezes-migration-progress--{chain}.json`
72+
73+
This file tracks:
74+
75+
- `pending_accounts`: Accounts still to be migrated
76+
- `migrated_accounts`: Successfully migrated accounts
77+
- `failed_accounts`: Failed migrations with error messages
78+
79+
You can safely interrupt and restart the script - it will resume from where it left off.
80+
81+
#### Verification
82+
83+
After each batch transaction, the script verifies migration by:
84+
85+
1. Checking `Balances.Freezes` for the account
86+
2. Confirming presence of `StakingCollator` or `StakingDelegator` freeze reason

0 commit comments

Comments
 (0)