diff --git a/src/backend-types/deso-types.ts b/src/backend-types/deso-types.ts index 5643b74..6425719 100644 --- a/src/backend-types/deso-types.ts +++ b/src/backend-types/deso-types.ts @@ -4616,6 +4616,42 @@ export interface AccessGroupMemberLimitMapItem { OpCount: number; } +export type StakeLimitMapItem = { + ValidatorPublicKeyBase58Check: string; + StakeLimit: string; // Hex string +}; + +export type UnstakeLimitMapItem = { + ValidatorPublicKeyBase58Check: string; + UnstakeLimit: string; // Hex string +}; + +export type UnlockStakeLimitMapItem = { + ValidatorPublicKeyBase58Check: string; + OpCount: number; +}; + +export enum LockupLimitScopeType { + ANY = 'AnyCoins', + SCOPED = 'ScopedCoins', +} + +export enum LockupLimitOperationString { + ANY = 'Any', + COIN_LOCKUP = 'CoinLockup', + UPDATE_COIN_LOCKUP_YIELD_CURVE = 'UpdateCoinLockupYieldCurve', + UPDATE_COIN_LOCKUP_TRANSFER_RESTRICTIONS = 'UpdateCoinLockupTransferRestrictions', + COIN_LOCKUP_TRANSFER = 'CoinLockupTransferOperationString', + COIN_UNLOCK = 'CoinLockupUnlock', +} + +export type LockupLimitMapItem = { + ProfilePublicKeyBase58Check: string; + ScopeType: LockupLimitScopeType; + Operation: LockupLimitOperationString; + OpCount: number; +}; + // struct2ts:types/generated/types.TransactionSpendingLimitResponse export interface TransactionSpendingLimitResponse { GlobalDESOLimit?: number; @@ -4627,6 +4663,10 @@ export interface TransactionSpendingLimitResponse { AssociationLimitMap?: AssociationLimitMapItem[]; AccessGroupLimitMap?: AccessGroupLimitMapItem[]; AccessGroupMemberLimitMap?: AccessGroupMemberLimitMapItem[]; + StakeLimitMap?: StakeLimitMapItem[]; + UnstakeLimitMap?: UnstakeLimitMapItem[]; + UnlockStakeLimitMap?: UnlockStakeLimitMapItem[]; + LockupLimitMap?: LockupLimitMapItem[]; IsUnlimited?: boolean; } @@ -5587,3 +5627,104 @@ export interface GetVideoStatusResponse { } export type DiamondLevelString = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8'; + +export interface RegisterAsValidatorRequest { + TransactorPublicKeyBase58Check: string; + Domains: string[]; + DelegatedStakeCommissionBasisPoints: number; + DisableDelegatedStake: boolean; + VotingPublicKey: string; + VotingAuthorization: string; + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface UnregisterAsValidatorRequest { + TransactorPublicKeyBase58Check: string; + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface UnjailValidatorRequest { + TransactorPublicKeyBase58Check: string; + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface ValidatorTxnResponse { + SpendAmountNanos: number; + TotalInputNanos: number; + ChangeAmountNanos: number; + FeeNanos: number; + Transaction: MsgDeSoTxn; + TransactionHex: string; + TxnHashHex: string; +} + +export interface ValidatorResponse { + ValidatorPublicKeyBase58Check: string; + Domains: string[]; + DisableDelegatedStake: boolean; + VotingPublicKey: string; + VotingAuthorization: string; + TotalStakeAmountNanos: string; // HEX STRING + Status: string; + LastActiveAtEpochNumber: number; + JailedAtEpochNumber: number; + ExtraData: Record; +} + +export enum StakeRewardMethod { + PayToBalance = 'PAY_TO_BALANCE', + Restake = 'RESTAKE', +} + +export interface StakeRequest { + TransactorPublicKeyBase58Check: string; + ValidatorPublicKeyBase58Check: string; + RewardMethod: StakeRewardMethod; + StakeAmountNanos: string; // HEX STRING + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface UnstakeRequest { + TransactorPublicKeyBase58Check: string; + ValidatorPublicKeyBase58Check: string; + UnstakeAmountNanos: string; // HEX STRING + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface UnlockStakeRequest { + TransactorPublicKeyBase58Check: string; + ValidatorPublicKeyBase58Check: string; + StartEpochNumber: number; + EndEpochNumber: number; + ExtraData: Record; + MinFeeRateNanosPerKB: number; + TransactionFees: TransactionFee[]; +} + +export interface StakeTxnResponse { + SpendAmountNanos: number; + TotalInputNanos: number; + ChangeAmountNanos: number; + FeeNanos: number; + Transaction: MsgDeSoTxn; + TransactionHex: string; + TxnHashHex: string; +} + +export interface StakeEntryResponse { + StakerPublicKeyBase58Check: string; + ValidatorPublicKeyBase58Check: string; + RewardMethod: StakeRewardMethod; + StakeAmountNanos: string; // HEX string + ExtraData: Record; +} diff --git a/src/identity/permissions-utils.ts b/src/identity/permissions-utils.ts index 3f424fb..06f73e3 100644 --- a/src/identity/permissions-utils.ts +++ b/src/identity/permissions-utils.ts @@ -94,6 +94,79 @@ export function compareTransactionSpendingLimits( : ['NFTOperationLimitMap', '', '0', path[path.length - 1]]; } break; + // TODO: support for making sure a derived key has these limits... + // @jacksondean - this is a little more annoying since + // stake and unstake limits don't have an op count, but rather a deso limit. + case 'StakeLimitMap': + if ( + actualPermissions?.StakeLimitMap?.find((map) => { + return ( + map.ValidatorPublicKeyBase58Check === '' && + expectedPermissions?.StakeLimitMap?.[Number(path[1])] + ?.StakeLimit && + parseInt(map.StakeLimit, 16) >= + parseInt( + expectedPermissions?.StakeLimitMap?.[Number(path[1])] + ?.StakeLimit, + 16 + ) + ); + }) + ) { + return; + } + break; + case 'UnstakeLimitMap': + if ( + actualPermissions?.UnstakeLimitMap?.find((map) => { + return ( + map.ValidatorPublicKeyBase58Check === '' && + expectedPermissions?.UnstakeLimitMap?.[Number(path[1])] + ?.UnstakeLimit && + parseInt(map.UnstakeLimit, 16) >= + parseInt( + expectedPermissions?.UnstakeLimitMap?.[Number(path[1])] + ?.UnstakeLimit, + 16 + ) + ); + }) + ) { + return; + } + break; + case 'UnlockStakeLimitMap': + if ( + actualPermissions?.UnlockStakeLimitMap?.find((map) => { + return ( + map.ValidatorPublicKeyBase58Check === '' && + map.OpCount >= + normalizeCount( + expectedPermissions?.UnlockStakeLimitMap?.[Number(path[1])] + ?.OpCount + ) + ); + }) + ) { + return; + } + break; + case 'LockupLimitMap': + if ( + actualPermissions?.LockupLimitMap?.find((map) => { + return ( + map.ProfilePublicKeyBase58Check === '' && + map.OpCount >= + normalizeCount( + expectedPermissions?.LockupLimitMap?.[Number(path[1])] + ?.OpCount + ) + ); + }) + ) { + return; + } + break; } const actualVal = getDeepValue(actualPermissions, path); @@ -166,6 +239,7 @@ export function buildTransactionSpendingLimitResponse( } }); } + // TODO: support for new PoS Spending limits maps. result.TransactionCountLimitMap = result.TransactionCountLimitMap ?? {}; diff --git a/src/identity/transaction-transcoders.ts b/src/identity/transaction-transcoders.ts index ba8b0b0..6eea3cd 100644 --- a/src/identity/transaction-transcoders.ts +++ b/src/identity/transaction-transcoders.ts @@ -22,6 +22,7 @@ import { VarBuffer, instanceToType, VarBufferArray, + BoolOptional, } from './transcoders.js'; export class TransactionInput extends BinaryRecord { @Transcode(FixedBuffer(32)) @@ -602,7 +603,7 @@ export class TransactionMetadataNewMessage extends BinaryRecord { export class TransactionMetadataRegisterAsValidator extends BinaryRecord { @Transcode(VarBufferArray) - domains: Buffer[] = []; + domains: Uint8Array[] = []; @Transcode(Boolean) disableDelegatedStake = false; @@ -615,42 +616,42 @@ export class TransactionMetadataRegisterAsValidator extends BinaryRecord { // The challenge is converting this into something human // readable in the UI. @Transcode(VarBuffer) - votingPublicKey: Buffer = Buffer.alloc(0); + votingPublicKey: Uint8Array = new Uint8Array(0); // TODO: Technically this is a bls signature, // but under the hood it's really just a byte array. // The challenge is converting this into something human // readable in the UI. @Transcode(VarBuffer) - votingAuthorization: Buffer = Buffer.alloc(0); + votingAuthorization: Uint8Array = new Uint8Array(0); } export class TransactionMetadataUnregisterAsValidator extends BinaryRecord {} export class TransactionMetadataStake extends BinaryRecord { @Transcode(VarBuffer) - validatorPublicKey: Buffer = Buffer.alloc(0); + validatorPublicKey: Uint8Array = new Uint8Array(0); @Transcode(Uint8) rewardMethod = 0; // TODO: We may want a better way to handle uint256s. - @Transcode(Optional(VarBuffer)) - stakeAmountNanos: Buffer = Buffer.alloc(0); + @Transcode(BoolOptional(VarBuffer)) + stakeAmountNanos: Uint8Array = new Uint8Array(0); } export class TransactionMetadataUnstake extends BinaryRecord { @Transcode(VarBuffer) - validatorPublicKey: Buffer = Buffer.alloc(0); + validatorPublicKey: Uint8Array = new Uint8Array(0); // TODO: We may want a better way to handle uint256s. - @Transcode(Optional(VarBuffer)) - unstakeAmountNanos: Buffer = Buffer.alloc(0); + @Transcode(BoolOptional(VarBuffer)) + unstakeAmountNanos: Uint8Array = new Uint8Array(0); } export class TransactionMetadataUnlockStake extends BinaryRecord { @Transcode(VarBuffer) - validatorPublicKey: Buffer = Buffer.alloc(0); + validatorPublicKey: Uint8Array = new Uint8Array(0); @Transcode(Uvarint64) startEpochNumber = 0; diff --git a/src/identity/transcoders.ts b/src/identity/transcoders.ts index 038caf7..54b063e 100644 --- a/src/identity/transcoders.ts +++ b/src/identity/transcoders.ts @@ -143,6 +143,26 @@ export function Optional(transcoder: Transcoder): Transcoder { }; } +export function BoolOptional( + transcoder: Transcoder +): Transcoder { + return { + read: (bytes: Uint8Array) => { + const existence = bytes.at(0) != 0; + if (!existence) { + return [null, bytes.slice(1)]; + } + return transcoder.read(bytes.slice(1)); + }, + write: (value: T | null) => { + if (value === null) { + return Uint8Array.from([0]); + } + return concatUint8Arrays([Uint8Array.from([1]), transcoder.write(value)]); + }, + }; +} + export const ChunkBuffer = (width: number): Transcoder => ({ read: (bytes) => { const countAndBuffer = bufToUvarint64(bytes); diff --git a/src/identity/types.ts b/src/identity/types.ts index 1149ef1..c11bffd 100644 --- a/src/identity/types.ts +++ b/src/identity/types.ts @@ -2,8 +2,12 @@ import { AccessGroupLimitMapItem, AccessGroupMemberLimitMapItem, AssociationLimitMapItem, + LockupLimitMapItem, + StakeLimitMapItem, TransactionSpendingLimitResponse, TransactionType, + UnlockStakeLimitMapItem, + UnstakeLimitMapItem, } from '../backend-types/index.js'; export type Network = 'mainnet' | 'testnet'; @@ -61,6 +65,18 @@ export interface TransactionSpendingLimitResponseOptions { AccessGroupMemberLimitMapItem, 'OpCount' > & { OpCount: number | 'UNLIMITED' })[]; + StakeLimitMap?: (Omit & { + StakeLimit: string | 'UNLIMITED'; // TODO: handle unlimited for DESO limit. + })[]; + UnstakeLimitMap?: (Omit & { + UnstakeLimit: string | 'UNLIMITED'; // TODO: handle unlimited for DESO limit. + })[]; + UnlockStakeLimitMap?: (Omit & { + OpCount: number | 'UNLIMITED'; + })[]; + LockupLimitMap?: (Omit & { + OpCount: number | 'UNLIMITED'; + })[]; IsUnlimited?: boolean; } diff --git a/src/transactions/stake.ts b/src/transactions/stake.ts new file mode 100644 index 0000000..3089c7d --- /dev/null +++ b/src/transactions/stake.ts @@ -0,0 +1,222 @@ +import { + ConstructedAndSubmittedTx, + TxRequestOptions, + TypeWithOptionalFeesAndExtraData, +} from '../types.js'; +import { + ConstructedTransactionResponse, + StakeRequest, + StakeRewardMethod, + StakeTxnResponse, + UnlockStakeRequest, + UnstakeRequest, +} from '../backend-types/index.js'; +import { + bs58PublicKeyToCompressedBytes, + TransactionMetadataStake, + TransactionMetadataUnlockStake, + TransactionMetadataUnstake, +} from '../identity/index.js'; +import { hexToBytes } from '@noble/hashes/utils'; +import { + constructBalanceModelTx, + getTxWithFeeNanos, + handleSignAndSubmit, + sumTransactionFees, +} from '../internal.js'; +import { guardTxPermission } from './utils.js'; + +type StakeRequestParams = TypeWithOptionalFeesAndExtraData; + +const buildStakeMetadata = (params: StakeRequestParams) => { + const metadata = new TransactionMetadataStake(); + metadata.validatorPublicKey = bs58PublicKeyToCompressedBytes( + params.ValidatorPublicKeyBase58Check + ); + metadata.rewardMethod = + params.RewardMethod === StakeRewardMethod.PayToBalance ? 0 : 1; + // TODO: make sure this replace is correct. + metadata.stakeAmountNanos = hexToBytes( + params.StakeAmountNanos.replace('0x', 'x') + ); + + return metadata; +}; + +export const constructStakeTransaction = ( + params: StakeRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildStakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const stake = async ( + params: StakeRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildStakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + parseInt(params.StakeAmountNanos, 16) + + txWithFee.feeNanos + + sumTransactionFees(params.TransactionFees), + StakeLimitMap: [ + { + ValidatorPublicKeyBase58Check: params.ValidatorPublicKeyBase58Check, + StakeLimit: params.StakeAmountNanos, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/stake', params, { + ...options, + constructionFunction: constructStakeTransaction, + }); +}; + +type UnstakeRequestParams = TypeWithOptionalFeesAndExtraData; + +const buildUnstakeMetadata = (params: UnstakeRequestParams) => { + const metadata = new TransactionMetadataUnstake(); + metadata.validatorPublicKey = bs58PublicKeyToCompressedBytes( + params.ValidatorPublicKeyBase58Check + ); + // TODO: make sure this replace is correct. + metadata.unstakeAmountNanos = hexToBytes( + params.UnstakeAmountNanos.replace('0x', 'x') + ); + + return metadata; +}; + +export const constructUnstakeTransaction = ( + params: UnstakeRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildUnstakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const unstake = async ( + params: UnstakeRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildUnstakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + UnstakeLimitMap: [ + { + ValidatorPublicKeyBase58Check: params.ValidatorPublicKeyBase58Check, + UnstakeLimit: params.UnstakeAmountNanos, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/unstake', params, { + ...options, + constructionFunction: constructUnstakeTransaction, + }); +}; + +type UnlockStakeRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildUnlockStakeMetadata = (params: UnlockStakeRequestParams) => { + const metadata = new TransactionMetadataUnlockStake(); + metadata.validatorPublicKey = bs58PublicKeyToCompressedBytes( + params.ValidatorPublicKeyBase58Check + ); + metadata.startEpochNumber = params.StartEpochNumber; + metadata.endEpochNumber = params.EndEpochNumber; + + return metadata; +}; + +export const constructUnlockStakeTransaction = ( + params: UnlockStakeRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildUnlockStakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const unlockStake = async ( + params: UnlockStakeRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildUnlockStakeMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + UnlockStakeLimitMap: [ + { + ValidatorPublicKeyBase58Check: params.ValidatorPublicKeyBase58Check, + OpCount: options?.txLimitCount ?? 1, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/unlock-stake', params, { + ...options, + constructionFunction: constructUnlockStakeTransaction, + }); +}; diff --git a/src/transactions/validator.ts b/src/transactions/validator.ts new file mode 100644 index 0000000..e27fde2 --- /dev/null +++ b/src/transactions/validator.ts @@ -0,0 +1,216 @@ +import { + ConstructedTransactionResponse, + RegisterAsValidatorRequest, + UnjailValidatorRequest, + UnregisterAsValidatorRequest, + ValidatorTxnResponse, +} from '../backend-types/index.js'; +import { + encodeUTF8ToBytes, + identity, + TransactionMetadataRegisterAsValidator, + TransactionMetadataUnjailValidator, + TransactionMetadataUnregisterAsValidator, +} from '../identity/index.js'; +import { hexToBytes } from '@noble/hashes/utils'; +import { + ConstructedAndSubmittedTx, + TxRequestOptions, + TypeWithOptionalFeesAndExtraData, +} from '../types.js'; +import { + constructBalanceModelTx, + getTxWithFeeNanos, + handleSignAndSubmit, + sumTransactionFees, +} from '../internal.js'; +import { guardTxPermission } from './utils.js'; + +type RegisterAsValidatorRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildRegisterAsValidatorMetadata = ( + params: RegisterAsValidatorRequestParams +) => { + const metadata = new TransactionMetadataRegisterAsValidator(); + metadata.domains = params.Domains.map((d) => encodeUTF8ToBytes(d)); + metadata.delegatedStakeCommissionBasisPoints = + params.DelegatedStakeCommissionBasisPoints; + metadata.disableDelegatedStake = params.DisableDelegatedStake; + metadata.votingPublicKey = hexToBytes(params.VotingPublicKey); + metadata.votingAuthorization = hexToBytes(params.VotingAuthorization); + + return metadata; +}; + +export const constructRegisterAsValidatorTransaction = ( + params: RegisterAsValidatorRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildRegisterAsValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const registerAsValidator = async ( + params: RegisterAsValidatorRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx< + ValidatorTxnResponse | ConstructedTransactionResponse + > +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildRegisterAsValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + TransactionCountLimitMap: { + REGISTER_AS_VALIDATOR: + options?.txLimitCount ?? + identity.transactionSpendingLimitOptions?.TransactionCountLimitMap + ?.REGISTER_AS_VALIDATOR ?? + 1, + }, + }); + } + + return handleSignAndSubmit('api/v0/validators/register', params, { + ...options, + constructionFunction: constructRegisterAsValidatorTransaction, + }); +}; + +type UnregisterAsValidatorRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildUnregisterAsValidatorMetadata = ( + params: UnregisterAsValidatorRequestParams +) => { + return new TransactionMetadataUnregisterAsValidator(); +}; + +export const constructUnregisterAsValidatorTransaction = ( + params: UnregisterAsValidatorRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildUnregisterAsValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const UnregisterAsValidator = async ( + params: UnregisterAsValidatorRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx< + ValidatorTxnResponse | ConstructedTransactionResponse + > +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildUnregisterAsValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + TransactionCountLimitMap: { + UNREGISTER_AS_VALIDATOR: + options?.txLimitCount ?? + identity.transactionSpendingLimitOptions?.TransactionCountLimitMap + ?.UNREGISTER_AS_VALIDATOR ?? + 1, + }, + }); + } + + return handleSignAndSubmit('api/v0/validators/unregister', params, { + ...options, + constructionFunction: constructUnregisterAsValidatorTransaction, + }); +}; + +type UnjailValidatorRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildUnjailValidatorMetadata = (params: UnjailValidatorRequestParams) => { + return new TransactionMetadataUnjailValidator(); +}; + +export const constructUnjailValidatorTransaction = ( + params: UnjailValidatorRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildUnjailValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const UnjailValidator = async ( + params: UnjailValidatorRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx< + ValidatorTxnResponse | ConstructedTransactionResponse + > +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildUnjailValidatorMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + TransactionCountLimitMap: { + UNJAIL_VALIDATOR: + options?.txLimitCount ?? + identity.transactionSpendingLimitOptions?.TransactionCountLimitMap + ?.UNREGISTER_AS_VALIDATOR ?? + 1, + }, + }); + } + + return handleSignAndSubmit('api/v0/validators/unjail', params, { + ...options, + constructionFunction: constructUnjailValidatorTransaction, + }); +};