diff --git a/package-lock.json b/package-lock.json index cfcecb3f8..b78a68794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "15.1.1", + "version": "15.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "15.1.1", + "version": "15.2.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index de492a4b5..63a17ec8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "15.1.1", + "version": "15.2.0", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/core/constants.ts b/src/core/constants.ts index b9dbfa59c..a398c7ef9 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -15,6 +15,7 @@ export const CONTRACT_DEPLOY_ADDRESS_HEX = "000000000000000000000000000000000000 export const DELEGATION_MANAGER_SC_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000004ffff"; export const ESDT_CONTRACT_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000002ffff"; export const GOVERNANCE_CONTRACT_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000003ffff"; +export const STAKING_SMART_CONTRACT_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000001ffff"; export const DEFAULT_MESSAGE_VERSION = 1; export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n"; diff --git a/src/core/transactionsFactoryConfig.ts b/src/core/transactionsFactoryConfig.ts index a7b38372f..53c7cc740 100644 --- a/src/core/transactionsFactoryConfig.ts +++ b/src/core/transactionsFactoryConfig.ts @@ -50,6 +50,22 @@ export class TransactionsFactoryConfig { gasLimitForClearProposals: bigint; gasLimitForChangeConfig: bigint; gasLimitForClaimAccumulatedFees: bigint; + gasLimitForStaking: bigint; + gasLimitForToppingUp: bigint; + gasLimitForUnstaking: bigint; + gasLimitForUnjailing: bigint; + gasLimitForUnbonding: bigint; + gasLimitForChangingRewardsAddress: bigint; + gasLimitForClaiming: bigint; + gasLimitForUnstakingNodes: bigint; + gasLimitForUnstakingTokens: bigint; + gasLimitForUnbondingNodes: bigint; + gasLimitForUnbondingTokens: bigint; + gasLimitForCleaningRegisteredData: bigint; + gasLimitForRestakingUnstakedTokens: bigint; + gasLimitForCreatingDelegationContractFromValidator: bigint; + gasLimitForWhitelistForMerge: bigint; + gasLimitForMergingValidatorToDelegation: bigint; constructor(options: { chainID: string }) { // General-purpose configuration @@ -114,5 +130,23 @@ export class TransactionsFactoryConfig { this.gasLimitForClearProposals = 50_000_000n; this.gasLimitForChangeConfig = 50_000_000n; this.gasLimitForClaimAccumulatedFees = 1_000_000n; + + // Configuration for staking operations + this.gasLimitForStaking = 5_000_000n; + this.gasLimitForToppingUp = 5_000_000n; + this.gasLimitForUnstaking = 5_000_000n; + this.gasLimitForUnjailing = 5_000_000n; + this.gasLimitForUnbonding = 5_000_000n; + this.gasLimitForChangingRewardsAddress = 5_000_000n; + this.gasLimitForClaiming = 5_000_000n; + this.gasLimitForUnstakingNodes = 5_000_000n; + this.gasLimitForUnstakingTokens = 5_000_000n; + this.gasLimitForUnbondingNodes = 5_000_000n; + this.gasLimitForUnbondingTokens = 5_000_000n; + this.gasLimitForCleaningRegisteredData = 5_000_000n; + this.gasLimitForRestakingUnstakedTokens = 5_000_000n; + this.gasLimitForCreatingDelegationContractFromValidator = 51_000_000n; + this.gasLimitForWhitelistForMerge = 5_000_000n; + this.gasLimitForMergingValidatorToDelegation = 50_000_000n; } } diff --git a/src/entrypoints/entrypoints.ts b/src/entrypoints/entrypoints.ts index e230d168c..ff15d0137 100644 --- a/src/entrypoints/entrypoints.ts +++ b/src/entrypoints/entrypoints.ts @@ -22,6 +22,8 @@ import { SmartContractTransactionsFactory } from "../smartContracts"; import { SmartContractController } from "../smartContracts/smartContractController"; import { TokenManagementController, TokenManagementTransactionsFactory } from "../tokenManagement"; import { TransfersController, TransferTransactionsFactory } from "../transfers"; +import { ValidatorsTransactionsFactory } from "../validators"; +import { ValidatorsController } from "../validators/validatorsController"; import { UserSecretKey } from "../wallet"; import { DevnetEntrypointConfig, MainnetEntrypointConfig, TestnetEntrypointConfig } from "./config"; @@ -260,6 +262,21 @@ export class NetworkEntrypoint { gasLimitEstimator: this.withGasLimitEstimator ? this.createGasLimitEstimator() : undefined, }); } + + createValidatorsController(): ValidatorsController { + return new ValidatorsController({ + chainID: this.chainId, + networkProvider: this.networkProvider, + gasLimitEstimator: this.withGasLimitEstimator ? this.createGasLimitEstimator() : undefined, + }); + } + + createValidatorsTransactionsFactory(): ValidatorsTransactionsFactory { + return new ValidatorsTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: this.chainId }), + gasLimitEstimator: this.withGasLimitEstimator ? this.createGasLimitEstimator() : undefined, + }); + } } export class TestnetEntrypoint extends NetworkEntrypoint { diff --git a/src/testdata/testwallets/validators.pem b/src/testdata/testwallets/validators.pem new file mode 100644 index 000000000..14bc776d8 --- /dev/null +++ b/src/testdata/testwallets/validators.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- +N2MxOWJmM2EwYzU3Y2RkMWZiMDhlNDYwN2NlYmFhMzY0N2Q2YjkyNjFiNDY5M2Y2 +MWU5NmU1NGIyMThkNDQyYQ== +-----END PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- +-----BEGIN PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- +MzAzNGIxZDU4NjI4YTg0Mjk4NGRhMGM3MGRhMGI1YTI1MWViYjJhZWJmNTFhZmM1 +YjU4NmUyODM5YjVlNTI2Mw== +-----END PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- diff --git a/src/validators/index.ts b/src/validators/index.ts new file mode 100644 index 000000000..4fe678845 --- /dev/null +++ b/src/validators/index.ts @@ -0,0 +1,2 @@ +export * from "./resources"; +export * from "./validatorsTransactionsFactory"; diff --git a/src/validators/resources.ts b/src/validators/resources.ts new file mode 100644 index 000000000..0f08b8b57 --- /dev/null +++ b/src/validators/resources.ts @@ -0,0 +1,15 @@ +import { Address } from "../core/address"; +import { ValidatorPublicKey } from "../wallet"; +import { ValidatorsSigners } from "./validatorsSigner"; + +export type StakingInput = { validatorsFile: ValidatorsSigners | string; amount: bigint; rewardsAddress?: Address }; +export type ChangingRewardsAddressInput = { rewardsAddress: Address }; +export type ToppingUpInput = { amount: bigint }; +export type UnstakingTokensInput = { amount: bigint }; +export type UnstakingInput = { publicKeys: ValidatorPublicKey[] }; +export type RestakingInput = { publicKeys: ValidatorPublicKey[] }; +export type UnbondingInput = { publicKeys: ValidatorPublicKey[] }; +export type UnbondingTokensInput = { amount: bigint }; +export type UnjailingInput = { publicKeys: ValidatorPublicKey[]; amount: bigint }; +export type NewDelegationContractInput = { maxCap: bigint; fee: bigint }; +export type MergeValidatorToDelegationInput = { delegationAddress: Address }; diff --git a/src/validators/validatorsController.spec.ts b/src/validators/validatorsController.spec.ts new file mode 100644 index 000000000..bff3ab792 --- /dev/null +++ b/src/validators/validatorsController.spec.ts @@ -0,0 +1,446 @@ +import { assert } from "chai"; +import path from "path"; +import { Account } from "../accounts"; +import { Address } from "../core"; +import { DELEGATION_MANAGER_SC_ADDRESS_HEX, STAKING_SMART_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { DevnetEntrypoint } from "../entrypoints/entrypoints"; +import { ValidatorPublicKey } from "../wallet"; +import { ValidatorsSigners } from "./validatorsSigner"; + +describe("test validator controller", function () { + const walletsPath = path.join("src", "testdata", "testwallets"); + const validatorsPath = path.join(walletsPath, "validators.pem"); + const entrypoint = new DevnetEntrypoint(); + let alice: Account; + + const rewardAddress = Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + const validatorPubkey = new ValidatorPublicKey( + Buffer.from( + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + "hex", + ), + ); + const validatorController = entrypoint.createValidatorsController(); + + beforeEach(async function () { + alice = await Account.newFromPem(path.join(walletsPath, "alice.pem")); + }); + + it("should create 'Transaction' for staking from file path", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForStaking( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + validatorsFile: validatorsPath, + amount: 2500000000000000000000n, + rewardsAddress: rewardAddress, + }, + ); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.version, 2); + assert.equal(transaction.gasLimit, 11029500n); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "stake@02@f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d@1865870f7f69162a2dfefd33fe232a9ca984c6f22d1ee3f6a5b34a8eb8c9f7319001f29d5a2eed85c1500aca19fa4189@1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d@12b309791213aac8ad9f34f0d912261e30f9ab060859e4d515e020a98b91d82a7cd334e4b504bb93d6b75347cccd6318@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + }); + + it("should create 'Transaction' for staking using validators file", async function () { + alice.nonce = 77777n; + const validatorsFile = await ValidatorsSigners.newFromPem(validatorsPath); + const transaction = await validatorController.createTransactionForStaking( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + validatorsFile: validatorsFile, + amount: 2500000000000000000000n, + rewardsAddress: rewardAddress, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.version, 2); + assert.equal(transaction.gasLimit, 11029500n); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "stake@02@f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d@1865870f7f69162a2dfefd33fe232a9ca984c6f22d1ee3f6a5b34a8eb8c9f7319001f29d5a2eed85c1500aca19fa4189@1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d@12b309791213aac8ad9f34f0d912261e30f9ab060859e4d515e020a98b91d82a7cd334e4b504bb93d6b75347cccd6318@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + }); + + it("should create 'Transaction' for topping up", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForToppingUp( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + amount: 2500000000000000000000n, + }, + ); + + assert.equal(transaction.sender.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5057500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "stake"); + }); + + it("should create 'Transaction' for unstake", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnstaking( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + publicKeys: [validatorPubkey], + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5350000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unStake@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unjail", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnjailing( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + publicKeys: [validatorPubkey], + amount: 2500000000000000000000n, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5348500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unJail@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for changing rewards address", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForChangingRewardsAddress( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + rewardsAddress: rewardAddress, + }, + ); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "changeRewardAddress@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for claiming", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForClaiming( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + {}, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5057500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "claim"); + }); + + it("should create 'Transaction' for unstaking nodes", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnstakingNodes( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + publicKeys: [validatorPubkey], + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5357500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unStakeNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unstaking tokens", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnstakingTokens( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + amount: 11000000000000000000n, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5095000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "unStakeTokens@98a7d9b8314c0000"); + }); + + it("should create 'Transaction' for unbonding nodes", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnbondingNodes( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + publicKeys: [validatorPubkey], + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5356000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unBondNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unbonding tokens", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForUnbondingTokens( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + amount: 20000000000000000000n, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5096500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "unBondTokens@01158e460913d00000"); + }); + + it("should create 'Transaction' for cleaning registered data", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForCleaningRegisteredData( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + {}, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5078500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "cleanRegisteredData"); + }); + + it("should create 'Transaction' for restaking unstaked nodes", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForRestakingUnstakedNodes( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + publicKeys: [validatorPubkey], + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5369500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "reStakeUnStakedNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for new delegation contract from validator", async function () { + alice.nonce = 77777n; + const transaction = await validatorController.createTransactionForNewDelegationContractFromValidatorData( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + maxCap: 0n, + fee: 3745n, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 51107000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "makeNewContractFromValidatorData@@0ea1"); + }); + + it("should create 'Transaction' for merging validator to delegation whitelisting", async function () { + alice.nonce = 77777n; + const delegationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc", + ); + + const transaction = await validatorController.createTransactionForMergingValidatorToDelegationWithWhitelist( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + delegationAddress: delegationContract, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 5206000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "mergeValidatorToDelegationWithWhitelist@000000000000000000010000000000000000000000000000000000002fffffff", + ); + }); + + it("should create 'Transaction' for merging validator to delegation same owner", async function () { + alice.nonce = 77777n; + const delegationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc", + ); + + const transaction = await validatorController.createTransactionForMergingValidatorToDelegationSameOwner( + alice, + BigInt(alice.getNonceThenIncrement().valueOf()), + { + delegationAddress: delegationContract, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.nonce, 77777n); + assert.equal(transaction.gasLimit, 50200000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "mergeValidatorToDelegationSameOwner@000000000000000000010000000000000000000000000000000000002fffffff", + ); + }); +}); diff --git a/src/validators/validatorsController.ts b/src/validators/validatorsController.ts new file mode 100644 index 000000000..57615d59a --- /dev/null +++ b/src/validators/validatorsController.ts @@ -0,0 +1,243 @@ +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + IGasLimitEstimator, + Transaction, + TransactionsFactoryConfig, +} from "../core"; +import { INetworkProvider } from "../networkProviders"; +import * as resources from "./resources"; +import { ValidatorsTransactionsFactory } from "./validatorsTransactionsFactory"; + +export class ValidatorsController extends BaseController { + private factory: ValidatorsTransactionsFactory; + + constructor(options: { + chainID: string; + networkProvider: INetworkProvider; + gasLimitEstimator?: IGasLimitEstimator; + }) { + super(); + this.factory = new ValidatorsTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + gasLimitEstimator: options.gasLimitEstimator, + }); + } + + async createTransactionForStaking( + sender: IAccount, + nonce: bigint, + options: resources.StakingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForStaking(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForToppingUp( + sender: IAccount, + nonce: bigint, + options: resources.ToppingUpInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForToppingUp(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnstaking( + sender: IAccount, + nonce: bigint, + options: resources.UnstakingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnstaking(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnjailing( + sender: IAccount, + nonce: bigint, + options: resources.UnjailingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnjailing(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnbonding( + sender: IAccount, + nonce: bigint, + options: resources.UnbondingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnbonding(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForChangingRewardsAddress( + sender: IAccount, + nonce: bigint, + options: resources.ChangingRewardsAddressInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForChangingRewardsAddress(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForClaiming( + sender: IAccount, + nonce: bigint, + options: BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForClaiming(sender.address); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnstakingNodes( + sender: IAccount, + nonce: bigint, + options: resources.UnstakingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnstakingNodes(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnstakingTokens( + sender: IAccount, + nonce: bigint, + options: resources.UnstakingTokensInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnstakingTokens(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnbondingNodes( + sender: IAccount, + nonce: bigint, + options: resources.UnbondingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnbondingNodes(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForUnbondingTokens( + sender: IAccount, + nonce: bigint, + options: resources.UnbondingTokensInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForUnbondingTokens(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForCleaningRegisteredData( + sender: IAccount, + nonce: bigint, + options: BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForCleaningRegisteredData(sender.address); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForRestakingUnstakedNodes( + sender: IAccount, + nonce: bigint, + options: resources.RestakingInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForRestakingUnstakedNodes(sender.address, options); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForNewDelegationContractFromValidatorData( + sender: IAccount, + nonce: bigint, + options: resources.NewDelegationContractInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForNewDelegationContractFromValidatorData( + sender.address, + options, + ); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForMergingValidatorToDelegationWithWhitelist( + sender: IAccount, + nonce: bigint, + options: resources.MergeValidatorToDelegationInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForMergingValidatorToDelegationWithWhitelist( + sender.address, + options, + ); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + async createTransactionForMergingValidatorToDelegationSameOwner( + sender: IAccount, + nonce: bigint, + options: resources.MergeValidatorToDelegationInput & BaseControllerInput, + ): Promise { + const transaction = await this.factory.createTransactionForMergingValidatorToDelegationSameOwner( + sender.address, + options, + ); + + await this.setupAndSignTransaction(transaction, options, nonce, sender); + + return transaction; + } + + private async setupAndSignTransaction( + transaction: Transaction, + options: BaseControllerInput, + nonce: bigint, + sender: IAccount, + ) { + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + } +} diff --git a/src/validators/validatorsSigner.ts b/src/validators/validatorsSigner.ts new file mode 100644 index 000000000..ca0ccb324 --- /dev/null +++ b/src/validators/validatorsSigner.ts @@ -0,0 +1,27 @@ +import { ValidatorPEM, ValidatorPublicKey, ValidatorSigner } from "../wallet"; + +export class ValidatorsSigners { + private signers: ValidatorSigner[]; + + constructor(validatorSigners: ValidatorSigner[]) { + this.signers = validatorSigners; + } + + static async newFromPem(filePath: string): Promise { + const validatorPemFiles = await ValidatorPEM.fromFileAll(filePath); + const signers = validatorPemFiles.map((pem) => new ValidatorSigner(pem.secretKey)); + return new ValidatorsSigners(signers); + } + + getNumOfNodes(): number { + return this.signers.length; + } + + getSigners(): ValidatorSigner[] { + return this.signers; + } + + getPublicKeys(): ValidatorPublicKey[] { + return this.signers.map((signer) => signer.getPubkey()); + } +} diff --git a/src/validators/validatorsTransactionsFactory.spec.ts b/src/validators/validatorsTransactionsFactory.spec.ts new file mode 100644 index 000000000..58c89cc16 --- /dev/null +++ b/src/validators/validatorsTransactionsFactory.spec.ts @@ -0,0 +1,348 @@ +import { assert } from "chai"; +import { TransactionsFactoryConfig } from "../core"; +import { Address } from "../core/address"; +import { DELEGATION_MANAGER_SC_ADDRESS_HEX, STAKING_SMART_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { getTestWalletsPath } from "../testutils"; +import { ValidatorPublicKey } from "../wallet"; +import { ValidatorsSigners } from "./validatorsSigner"; +import { ValidatorsTransactionsFactory } from "./validatorsTransactionsFactory"; + +describe("test validator transactions factory", function () { + const config = new TransactionsFactoryConfig({ chainID: "D" }); + const validatorsFactory = new ValidatorsTransactionsFactory({ config: config }); + const validatorsPath = `${getTestWalletsPath()}/validators.pem`; + const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const rewardAddress = Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + const validatorPubkey = new ValidatorPublicKey( + Buffer.from( + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + "hex", + ), + ); + + it("should create 'Transaction' for staking from file path", async function () { + const transaction = await validatorsFactory.createTransactionForStaking(alice, { + validatorsFile: validatorsPath, + amount: 2500000000000000000000n, + rewardsAddress: rewardAddress, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.version, 2); + assert.equal(transaction.gasLimit, 11029500n); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "stake@02@f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d@1865870f7f69162a2dfefd33fe232a9ca984c6f22d1ee3f6a5b34a8eb8c9f7319001f29d5a2eed85c1500aca19fa4189@1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d@12b309791213aac8ad9f34f0d912261e30f9ab060859e4d515e020a98b91d82a7cd334e4b504bb93d6b75347cccd6318@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + }); + + it("should create 'Transaction' for staking using validators file", async function () { + const validatorsFile = await ValidatorsSigners.newFromPem(validatorsPath); + const transaction = await validatorsFactory.createTransactionForStaking(alice, { + validatorsFile: validatorsFile, + amount: 2500000000000000000000n, + rewardsAddress: rewardAddress, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.version, 2); + assert.equal(transaction.gasLimit, 11029500n); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "stake@02@f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d@1865870f7f69162a2dfefd33fe232a9ca984c6f22d1ee3f6a5b34a8eb8c9f7319001f29d5a2eed85c1500aca19fa4189@1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d@12b309791213aac8ad9f34f0d912261e30f9ab060859e4d515e020a98b91d82a7cd334e4b504bb93d6b75347cccd6318@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + }); + + it("should create 'Transaction' for topping up", async function () { + const transaction = await validatorsFactory.createTransactionForToppingUp(alice, { + amount: 2500000000000000000000n, + }); + + assert.equal(transaction.sender.toBech32(), "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 2500000000000000000000n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5057500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "stake"); + }); + + it("should create 'Transaction' for unstake", async function () { + const transaction = await validatorsFactory.createTransactionForUnstaking(alice, { + publicKeys: [validatorPubkey], + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5350000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unStake@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unjail", async function () { + const transaction = await validatorsFactory.createTransactionForUnjailing(alice, { + publicKeys: [validatorPubkey], + amount: 2500000000000000000000n, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5348500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unJail@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for changing rewards address", async function () { + const transaction = await validatorsFactory.createTransactionForChangingRewardsAddress(alice, { + rewardsAddress: rewardAddress, + }); + + assert.deepEqual( + transaction.sender, + Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"), + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.isDefined(transaction.data); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "changeRewardAddress@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + ); + assert.equal(transaction.value, 0n); + }); + + it("should create 'Transaction' for claiming", async function () { + const transaction = await validatorsFactory.createTransactionForClaiming(alice); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5057500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "claim"); + }); + + it("should create 'Transaction' for unstaking nodes", async function () { + const transaction = await validatorsFactory.createTransactionForUnstakingNodes(alice, { + publicKeys: [validatorPubkey], + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5357500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unStakeNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unstaking tokens", async function () { + const transaction = await validatorsFactory.createTransactionForUnstakingTokens(alice, { + amount: 11000000000000000000n, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5095000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "unStakeTokens@98a7d9b8314c0000"); + }); + + it("should create 'Transaction' for unbonding nodes", async function () { + const transaction = await validatorsFactory.createTransactionForUnbondingNodes(alice, { + publicKeys: [validatorPubkey], + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5356000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "unBondNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for unbonding tokens", async function () { + const transaction = await validatorsFactory.createTransactionForUnbondingTokens(alice, { + amount: 20000000000000000000n, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5096500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "unBondTokens@01158e460913d00000"); + }); + + it("should create 'Transaction' for cleaning registered data", async function () { + const transaction = await validatorsFactory.createTransactionForCleaningRegisteredData(alice); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5078500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "cleanRegisteredData"); + }); + + it("should create 'Transaction' for restaking unstaked nodes", async function () { + const transaction = await validatorsFactory.createTransactionForRestakingUnstakedNodes(alice, { + publicKeys: [validatorPubkey], + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5369500n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "reStakeUnStakedNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + }); + + it("should create 'Transaction' for new delegation contract from validator", async function () { + const transaction = await validatorsFactory.createTransactionForNewDelegationContractFromValidatorData(alice, { + maxCap: 0n, + fee: 3745n, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 51107000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual(Buffer.from(transaction.data).toString(), "makeNewContractFromValidatorData@@0ea1"); + }); + + it("should create 'Transaction' for merging validator to delegation whitelisting", async function () { + const delegationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc", + ); + + const transaction = await validatorsFactory.createTransactionForMergingValidatorToDelegationWithWhitelist( + alice, + { + delegationAddress: delegationContract, + }, + ); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 5206000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "mergeValidatorToDelegationWithWhitelist@000000000000000000010000000000000000000000000000000000002fffffff", + ); + }); + + it("should create 'Transaction' for merging validator to delegation same owner", async function () { + const delegationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqtllllls002zgc", + ); + + const transaction = await validatorsFactory.createTransactionForMergingValidatorToDelegationSameOwner(alice, { + delegationAddress: delegationContract, + }); + + assert.deepEqual( + transaction.sender.toBech32(), + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ); + assert.deepEqual(transaction.receiver, Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX)); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, "D"); + assert.equal(transaction.gasLimit, 50200000n); + assert.equal(transaction.version, 2); + assert.equal(transaction.options, 0); + assert.deepEqual( + Buffer.from(transaction.data).toString(), + "mergeValidatorToDelegationSameOwner@000000000000000000010000000000000000000000000000000000002fffffff", + ); + }); +}); diff --git a/src/validators/validatorsTransactionsFactory.ts b/src/validators/validatorsTransactionsFactory.ts new file mode 100644 index 000000000..5532c590f --- /dev/null +++ b/src/validators/validatorsTransactionsFactory.ts @@ -0,0 +1,393 @@ +import { AddressValue, ArgSerializer, BigUIntValue, BytesValue, U32Value } from "../abi"; +import { IGasLimitEstimator, TransactionsFactoryConfig } from "../core"; +import { Address } from "../core/address"; +import { BaseFactory } from "../core/baseFactory"; +import { DELEGATION_MANAGER_SC_ADDRESS_HEX, STAKING_SMART_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { Transaction } from "../core/transaction"; +import * as resources from "./resources"; +import { ValidatorsSigners } from "./validatorsSigner"; + +/** + * Use this class to create validators related transactions like creating transaction for staking or adding nodes. + */ +export class ValidatorsTransactionsFactory extends BaseFactory { + private readonly config: TransactionsFactoryConfig; + private readonly argSerializer: ArgSerializer; + + constructor(options: { config: TransactionsFactoryConfig; gasLimitEstimator?: IGasLimitEstimator }) { + super({ config: options.config, gasLimitEstimator: options.gasLimitEstimator }); + this.config = options.config; + this.argSerializer = new ArgSerializer(); + } + + async createTransactionForStaking(sender: Address, options: resources.StakingInput): Promise { + let validators: ValidatorsSigners; + if (typeof options.validatorsFile === "string") { + validators = await ValidatorsSigners.newFromPem(options.validatorsFile); + } else { + validators = options.validatorsFile; + } + + const dataParts = this.prepareDataPartsForStaking({ + nodeOperator: sender, + validatorsFile: validators, + rewardsAddress: options.rewardsAddress, + }); + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + value: options.amount, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForStaking * BigInt(validators.getNumOfNodes()), + ); + + return transaction; + } + + private prepareDataPartsForStaking(options: { + nodeOperator: Address; + validatorsFile: ValidatorsSigners; + rewardsAddress: Address | undefined; + }) { + const dataParts = ["stake"]; + const numOfNodes = options.validatorsFile.getNumOfNodes(); + + const callArguments = []; + callArguments.push(new U32Value(numOfNodes)); + + for (const signer of options.validatorsFile.getSigners()) { + const signedMessages = signer.sign(options.nodeOperator.getPublicKey()); + callArguments.push(new BytesValue(Buffer.from(signer.getPubkey()))); + callArguments.push(new BytesValue(Buffer.from(signedMessages))); + } + + if (options.rewardsAddress) { + callArguments.push(new AddressValue(options.rewardsAddress)); + } + + const args = this.argSerializer.valuesToStrings(callArguments); + return dataParts.concat(args); + } + + async createTransactionForToppingUp(sender: Address, options: resources.ToppingUpInput): Promise { + const data = ["stake"]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + value: options.amount, + }); + + this.setTransactionPayload(transaction, data); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForToppingUp); + + return transaction; + } + + async createTransactionForUnstaking(sender: Address, options: resources.UnstakingInput): Promise { + const dataParts = ["unStake"]; + + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnstaking * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForUnjailing(sender: Address, options: resources.UnjailingInput): Promise { + const dataParts = ["unJail"]; + + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnjailing * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForUnbonding(sender: Address, options: resources.UnbondingInput): Promise { + const dataParts = ["unBond"]; + + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnbonding * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForChangingRewardsAddress( + sender: Address, + options: resources.ChangingRewardsAddressInput, + ): Promise { + const dataParts = ["changeRewardAddress", options.rewardsAddress.toHex()]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForChangingRewardsAddress); + + return transaction; + } + + async createTransactionForClaiming(sender: Address): Promise { + const dataParts = ["claim"]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForClaiming); + + return transaction; + } + + async createTransactionForUnstakingNodes(sender: Address, options: resources.UnstakingInput): Promise { + const dataParts = ["unStakeNodes"]; + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnstakingNodes * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForUnstakingTokens( + sender: Address, + options: resources.UnstakingTokensInput, + ): Promise { + const dataParts = ["unStakeTokens", this.argSerializer.valuesToStrings([new BigUIntValue(options.amount)])[0]]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForUnstakingTokens); + + return transaction; + } + + async createTransactionForUnbondingNodes(sender: Address, options: resources.UnbondingInput): Promise { + const dataParts = ["unBondNodes"]; + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnbondingNodes * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForUnbondingTokens( + sender: Address, + options: resources.UnbondingTokensInput, + ): Promise { + const dataParts = ["unBondTokens", this.argSerializer.valuesToStrings([new BigUIntValue(options.amount)])[0]]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForUnbondingTokens); + + return transaction; + } + + async createTransactionForCleaningRegisteredData(sender: Address): Promise { + const dataParts = ["cleanRegisteredData"]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForCleaningRegisteredData); + + return transaction; + } + + async createTransactionForRestakingUnstakedNodes( + sender: Address, + options: resources.RestakingInput, + ): Promise { + const dataParts = ["reStakeUnStakedNodes"]; + for (const key of options.publicKeys) { + dataParts.push(key.hex()); + } + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(STAKING_SMART_CONTRACT_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit( + transaction, + undefined, + this.config.gasLimitForUnstakingNodes * BigInt(options.publicKeys.length), + ); + + return transaction; + } + + async createTransactionForNewDelegationContractFromValidatorData( + sender: Address, + options: resources.NewDelegationContractInput, + ): Promise { + const dataParts = [ + "makeNewContractFromValidatorData", + ...this.argSerializer.valuesToStrings([new BigUIntValue(options.maxCap), new BigUIntValue(options.fee)]), + ]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForCreatingDelegationContractFromValidator); + + return transaction; + } + + async createTransactionForMergingValidatorToDelegationWithWhitelist( + sender: Address, + options: resources.MergeValidatorToDelegationInput, + ): Promise { + const dataParts = [ + "mergeValidatorToDelegationWithWhitelist", + this.argSerializer.valuesToStrings([new AddressValue(options.delegationAddress)])[0], + ]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForWhitelistForMerge); + + return transaction; + } + + async createTransactionForMergingValidatorToDelegationSameOwner( + sender: Address, + options: resources.MergeValidatorToDelegationInput, + ): Promise { + const dataParts = [ + "mergeValidatorToDelegationSameOwner", + this.argSerializer.valuesToStrings([new AddressValue(options.delegationAddress)])[0], + ]; + + const transaction = new Transaction({ + sender, + receiver: Address.newFromHex(DELEGATION_MANAGER_SC_ADDRESS_HEX, this.config.addressHrp), + chainID: this.config.chainID, + gasLimit: 0n, + }); + + this.setTransactionPayload(transaction, dataParts); + await this.setGasLimit(transaction, undefined, this.config.gasLimitForMergingValidatorToDelegation); + + return transaction; + } +} diff --git a/src/wallet/index.ts b/src/wallet/index.ts index a0181b16d..da2cc8a5f 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -8,4 +8,5 @@ export * from "./userSigner"; export * from "./userVerifier"; export * from "./userWallet"; export * from "./validatorKeys"; +export * from "./validatorPem"; export * from "./validatorSigner"; diff --git a/src/wallet/validatorPem.spec.ts b/src/wallet/validatorPem.spec.ts new file mode 100644 index 000000000..746ef9635 --- /dev/null +++ b/src/wallet/validatorPem.spec.ts @@ -0,0 +1,71 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import path from "path"; +import { getTestWalletsPath } from "../testutils"; +import { ValidatorPEM } from "./validatorPem"; + +describe("test ValidatorPEMs", () => { + const walletsPath = path.join("src", "testdata", "testwallets"); + const pemPath = `${getTestWalletsPath()}/validatorKey00.pem`; + const savedPath = pemPath.replace(".pem", "-saved.pem"); + + afterEach(() => { + if (fs.existsSync(savedPath)) { + fs.unlinkSync(savedPath); // cleanup + } + }); + + it("should save pem from file", async function () { + const contentExpected = fs.readFileSync(pemPath, "utf-8").trim(); + + // fromFile is async → await + const pem = await ValidatorPEM.fromFile(pemPath); + pem.save(savedPath); + + const contentActual = fs.readFileSync(savedPath, "utf-8").trim(); + assert.deepEqual(contentActual, contentExpected); + }); + + it("should create from text all", async function () { + let text = fs.readFileSync(path.join(walletsPath, "validatorKey00.pem"), "utf-8"); + + let entries = await ValidatorPEM.fromTextAll(text); + let entry = entries[0]; + + assert.lengthOf(entries, 1); + assert.equal( + entry.label, + "e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208", + ); + assert.equal(entry.secretKey.hex(), "7cff99bd671502db7d15bc8abc0c9a804fb925406fbdd50f1e4c17a4cd774247"); + + text = fs.readFileSync(path.join(walletsPath, "validators.pem"), "utf-8"); + entries = await ValidatorPEM.fromTextAll(text); + entry = entries[0]; + + assert.lengthOf(entries, 2); + assert.equal( + entry.label, + "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", + ); + assert.equal(entry.secretKey.hex(), "7c19bf3a0c57cdd1fb08e4607cebaa3647d6b9261b4693f61e96e54b218d442a"); + + entry = entries[1]; + assert.equal( + entry.label, + "1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d", + ); + assert.equal(entry.secretKey.hex(), "3034b1d58628a842984da0c70da0b5a251ebb2aebf51afc5b586e2839b5e5263"); + }); + + it("should convert to text", async function () { + let text = fs.readFileSync(path.join(walletsPath, "validatorKey00.pem"), "utf-8").trim(); + const entry = await ValidatorPEM.fromTextAll(text); + assert.deepEqual(entry[0].toText(), text); + + text = fs.readFileSync(path.join(walletsPath, "multipleValidatorKeys.pem"), "utf-8").trim(); + const entries = await ValidatorPEM.fromTextAll(text); + const actualText = entries.map((entry) => entry.toText()).join("\n"); + assert.deepEqual(actualText, text); + }); +}); diff --git a/src/wallet/validatorPem.ts b/src/wallet/validatorPem.ts new file mode 100644 index 000000000..b7e07f56d --- /dev/null +++ b/src/wallet/validatorPem.ts @@ -0,0 +1,52 @@ +import { readFileSync, writeFileSync } from "fs"; +import { resolve } from "path"; +import { PemEntry } from "../wallet/pemEntry"; +import { BLS, ValidatorSecretKey } from "../wallet/validatorKeys"; + +export class ValidatorPEM { + readonly label: string; + readonly secretKey: ValidatorSecretKey; + + constructor(label: string, secretKey: ValidatorSecretKey) { + this.label = label; + this.secretKey = secretKey; + } + + static async fromFile(path: string, index = 0): Promise { + return (await this.fromFileAll(path))[index]; + } + + static async fromFileAll(path: string): Promise { + const absPath = resolve(path); + const text = readFileSync(absPath, "utf-8"); + return await this.fromTextAll(text); + } + + static async fromText(text: string, index = 0): Promise { + return (await ValidatorPEM.fromTextAll(text))[index]; + } + + static async fromTextAll(text: string): Promise { + const entries = PemEntry.fromTextAll(text); + const resultItems: ValidatorPEM[] = []; + + await BLS.initIfNecessary(); + for (const entry of entries) { + const secretKey = new ValidatorSecretKey(entry.message); + const item = new ValidatorPEM(entry.label, secretKey); + resultItems.push(item); + } + + return resultItems; + } + + save(path: string): void { + const absPath = resolve(path); + writeFileSync(absPath, this.toText(), "utf-8"); + } + + toText(): string { + const message = this.secretKey.valueOf(); + return new PemEntry(this.label, message).toText(); + } +} diff --git a/src/wallet/validatorSigner.ts b/src/wallet/validatorSigner.ts index 41122980f..ff684796b 100644 --- a/src/wallet/validatorSigner.ts +++ b/src/wallet/validatorSigner.ts @@ -1,14 +1,26 @@ import { ErrSignerCannotSign } from "../core/errors"; -import { BLS, ValidatorSecretKey } from "./validatorKeys"; +import { BLS, ValidatorPublicKey, ValidatorSecretKey } from "./validatorKeys"; +import { ValidatorPEM } from "./validatorPem"; /** * Validator signer (BLS signer) */ export class ValidatorSigner { + private readonly secretKey: ValidatorSecretKey; + + constructor(secretKey: ValidatorSecretKey) { + this.secretKey = secretKey; + } + /** + * * @deprecated This method will be deprecated! Use the sign method directly. * Signs a message. */ - async signUsingPem(pemText: string, pemIndex: number = 0, signable: Buffer | Uint8Array): Promise { + static async signUsingPem( + pemText: string, + pemIndex: number = 0, + signable: Buffer | Uint8Array, + ): Promise { await BLS.initIfNecessary(); try { @@ -18,4 +30,25 @@ export class ValidatorSigner { throw new ErrSignerCannotSign(err); } } + + static async fromPemFile(path: string, index = 0): Promise { + const secretKey = (await ValidatorPEM.fromFile(path, index)).secretKey; + return new ValidatorSigner(secretKey); + } + + sign(data: Uint8Array): Uint8Array { + try { + return this.trySign(data); + } catch (err) { + throw new ErrSignerCannotSign(err as Error); + } + } + + private trySign(data: Uint8Array): Uint8Array { + return this.secretKey.sign(data); + } + + getPubkey(): ValidatorPublicKey { + return this.secretKey.generatePublicKey(); + } }