Skip to content

Commit 1388171

Browse files
committed
feat: add consolidation hash generator
1 parent 6fc2f4d commit 1388171

File tree

11 files changed

+1906
-2106
lines changed

11 files changed

+1906
-2106
lines changed

features/consolidation.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { retryCall } from 'utils';
2+
import Safe from '@safe-global/protocol-kit';
3+
import { MetaTransactionData, OperationType } from '@safe-global/types-kit';
4+
import { config } from 'dotenv';
5+
import { ethers } from 'ethers';
6+
config();
7+
8+
export const getSafeTxHash = async (
9+
safe: Safe,
10+
sourcePubkeys: string[][],
11+
targetPubkeys: string[],
12+
refundRecipient: string,
13+
) => {
14+
const calldata = consolidationRequestCalldata(
15+
sourcePubkeys,
16+
targetPubkeys,
17+
refundRecipient,
18+
);
19+
20+
const consolidationContract = process.env.CONSOLIDATION_CONTRACT;
21+
if (!consolidationContract) {
22+
throw new Error('Missing CONSOLIDATION_CONTRACT environment variable');
23+
}
24+
25+
const safeTransactionData: MetaTransactionData = {
26+
to: consolidationContract,
27+
value: '1',
28+
data: calldata,
29+
operation: OperationType.DelegateCall,
30+
};
31+
32+
const safeTransaction = await retryCall(async () => {
33+
return await safe.createTransaction({
34+
transactions: [safeTransactionData],
35+
});
36+
});
37+
38+
const safeTxHash = await retryCall(async () => {
39+
return await safe.getTransactionHash(safeTransaction);
40+
});
41+
42+
return safeTxHash;
43+
};
44+
45+
const consolidationRequestCalldata = (
46+
sourcePubkeys: string[][],
47+
targetPubkeys: string[],
48+
refundRecipient: string,
49+
): string => {
50+
const abi = [
51+
{
52+
name: 'addConsolidationRequests',
53+
type: 'function',
54+
stateMutability: 'payable',
55+
inputs: [
56+
{ name: '_sourcePubkeys', type: 'bytes[]' },
57+
{ name: '_targetPubkeys', type: 'bytes[]' },
58+
{ name: '_refundRecipient', type: 'address' },
59+
],
60+
outputs: [],
61+
},
62+
];
63+
64+
const sourcePubkeysFormatted = flattenInnerHexGroups(sourcePubkeys);
65+
const iface = new ethers.Interface(abi);
66+
67+
const calldata = iface.encodeFunctionData('addConsolidationRequests', [
68+
sourcePubkeysFormatted,
69+
targetPubkeys,
70+
refundRecipient,
71+
]);
72+
return calldata;
73+
};
74+
75+
const flattenInnerHexGroups = (nested: string[][]): string[] => {
76+
return nested.map(
77+
(group) => '0x' + group.map((p) => p.replace(/^0x/, '')).join(''),
78+
);
79+
};

programs/use-cases/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './vault-operations/index.js';
22
export * from './deposits/index.js';
33
export * from './report/index.js';
44
export * from './metrics/index.js';
5+
export * from './validators/index.js';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './main.js';
2+
export * from './read.js';
3+
export * from './write.js';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { program } from 'command';
2+
3+
export const validators = program
4+
.command('validators')
5+
.description('validators utilities');
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
logInfo,
3+
retryCall,
4+
stringToAddress,
5+
stringTo2dArray,
6+
stringToArray,
7+
} from 'utils';
8+
import { Address } from 'viem';
9+
import { validators } from './main.js';
10+
import { getSafeTxHash } from '../../../features/consolidation.js';
11+
import Safe from '@safe-global/protocol-kit';
12+
13+
const validatorsRead = validators
14+
.command('read')
15+
.aliases(['r'])
16+
.description('validators read commands');
17+
18+
validatorsRead
19+
.command('consolidation_tx_hash')
20+
.description('get consolidation transaction hash')
21+
.argument('<safe_address>', 'safe address', stringToAddress)
22+
.argument('<consolidation_address>', 'consolidation address', stringToAddress)
23+
.argument('<refund_recipient>', 'refund recipient address', stringToAddress)
24+
.argument(
25+
'<source_pubkeys>',
26+
'source pubkeys of the validators to consolidate from. Comma separated list of lists pubkeys',
27+
stringTo2dArray,
28+
)
29+
.argument(
30+
'<target_pubkeys>',
31+
'pubkeys of the validators to withdraw. Comma separated list of pubkeys',
32+
stringToArray,
33+
)
34+
.action(
35+
async (
36+
safeAddress: Address,
37+
consolidationAddress: Address,
38+
refundRecipient: Address,
39+
source_pubkeys: string[][],
40+
target_pubkeys: string[],
41+
) => {
42+
logInfo('get consolidation hash');
43+
logInfo('safeAddress:', safeAddress);
44+
logInfo('consolidationAddress:', consolidationAddress);
45+
logInfo('refundRecipient:', refundRecipient);
46+
logInfo('source_pubkeys:', source_pubkeys);
47+
logInfo('target_pubkeys:', target_pubkeys);
48+
49+
const provider = process.env.RPC_URL;
50+
if (!provider) {
51+
throw new Error('Missing RPC_URL environment variable');
52+
}
53+
54+
const safe = await retryCall(async () => {
55+
return await Safe.init({
56+
provider: provider,
57+
safeAddress: safeAddress,
58+
});
59+
});
60+
61+
const safeTxHash = await getSafeTxHash(
62+
safe,
63+
source_pubkeys,
64+
target_pubkeys,
65+
refundRecipient,
66+
);
67+
logInfo('Safe tx hash:', safeTxHash);
68+
},
69+
);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
logInfo,
3+
retryCall,
4+
stringTo2dArray,
5+
stringToAddress,
6+
stringToArray,
7+
} from 'utils';
8+
import { validators } from './main.js';
9+
import Safe from '@safe-global/protocol-kit';
10+
import { getSafeTxHash } from '../../../features/consolidation.js';
11+
import { Address } from 'viem';
12+
13+
const validatorsWrite = validators
14+
.command('write')
15+
.aliases(['w'])
16+
.description('validators write commands');
17+
18+
validatorsWrite
19+
.command('consolidate')
20+
.description('consolidate validators')
21+
.argument('<safe_address>', 'safe address', stringToAddress)
22+
.argument('<consolidation_address>', 'consolidation address', stringToAddress)
23+
.argument('<refund_recipient>', 'refund recipient address', stringToAddress)
24+
.argument(
25+
'<source_pubkeys>',
26+
'source pubkeys of the validators to consolidate from. Comma separated list of lists pubkeys',
27+
stringTo2dArray,
28+
)
29+
.argument(
30+
'<target_pubkeys>',
31+
'pubkeys of the validators to withdraw. Comma separated list of pubkeys',
32+
stringToArray,
33+
)
34+
.action(
35+
async (
36+
safeAddress: Address,
37+
consolidationAddress: Address,
38+
refundRecipient: Address,
39+
sourcePubkeys: string[][],
40+
targetPubkeys: string[],
41+
) => {
42+
logInfo('create and approve consolidation hash');
43+
logInfo('safeAddress:', safeAddress);
44+
logInfo('consolidationAddress:', consolidationAddress);
45+
logInfo('refundRecipient:', refundRecipient);
46+
logInfo('sourcePubkeys:', sourcePubkeys);
47+
logInfo('targetPubkeys:', targetPubkeys);
48+
49+
const provider = process.env.RPC_URL;
50+
if (!provider) {
51+
throw new Error('Missing RPC_URL environment variable');
52+
}
53+
54+
const signer = process.env.OWNER_PRIVATE_KEY;
55+
if (!signer) {
56+
throw new Error('Missing OWNER_PRIVATE_KEY environment variable');
57+
}
58+
59+
const safe = await retryCall(async () => {
60+
return await Safe.init({
61+
provider: provider,
62+
signer: signer,
63+
safeAddress: safeAddress,
64+
});
65+
});
66+
logInfo('Safe instance initialized');
67+
68+
const safeTxHash = await getSafeTxHash(
69+
safe,
70+
sourcePubkeys,
71+
targetPubkeys,
72+
refundRecipient,
73+
);
74+
logInfo('Safe tx hash:');
75+
logInfo(safeTxHash);
76+
77+
const signature = await retryCall(async () => {
78+
return await safe.signHash(safeTxHash);
79+
});
80+
81+
logInfo('Signature:');
82+
logInfo(signature);
83+
84+
logInfo('Proposing transaction to the service');
85+
86+
await retryCall(async () => {
87+
await safe.approveTransactionHash(safeTxHash);
88+
});
89+
},
90+
);

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"declaration": true,
44
"lib": ["DOM", "ES2022"],
55
"target": "ESNext",
6-
"moduleResolution": "NodeNext",
76
"useDefineForClassFields": true, // Not enabled by default in `strict` mode unless we bump `target` to ES2022.
87
"noFallthroughCasesInSwitch": true, // Not enabled by default in `strict` mode.
98
"noImplicitReturns": true, // Not enabled by default in `strict` mode.
@@ -34,7 +33,8 @@
3433
"outDir": "dist",
3534
"sourceMap": true,
3635
"rootDir": ".",
37-
"module": "NodeNext"
36+
"module": "ESNext",
37+
"moduleResolution": "Node"
3838
},
3939
"ts-node": {
4040
"swc": true,

utils/arguments.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ export const stringToBigIntArrayWei = (value: string) => {
1515
return value.split(',').map(etherToWei);
1616
};
1717

18+
export const stringTo2dArray = (value: string): string[][] => {
19+
const trimmed = value.replace(/^["']|["']$/g, '');
20+
return trimmed
21+
.split(',')
22+
.map((group) => group.trim().split(/\s+/).filter(Boolean));
23+
};
24+
25+
export const stringToArray = (value: string): string[] => {
26+
const trimmed = value.replace(/^["']|["']$/g, '');
27+
return trimmed.split(',');
28+
};
29+
1830
export const stringToHexArray = (value: string) => {
1931
return value.split(',').map(toHex);
2032
};

utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ export * from './consts.js';
3232
export * from './snake-to-camel.js';
3333
export * from './timestamp.js';
3434
export * from './wallet-connect.js';
35-
export * from './csv-file.js';
35+
export * from './csv-file.js';

utils/retry-call.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { logInfo } from 'utils';
2+
3+
export const retryCall = async (
4+
fn: () => Promise<any>,
5+
retries = 5,
6+
delay = 2000,
7+
) => {
8+
while (retries > 0) {
9+
try {
10+
return await fn();
11+
} catch (error) {
12+
logInfo(`Failed, retrying in ${delay}ms...`);
13+
logInfo(error);
14+
await new Promise((resolve) => setTimeout(resolve, delay));
15+
retries--;
16+
}
17+
}
18+
};

0 commit comments

Comments
 (0)