From e454c61994caf4031bab893c74eafd5c370513b9 Mon Sep 17 00:00:00 2001 From: tequ Date: Fri, 17 Oct 2025 17:44:07 +0900 Subject: [PATCH] Support Cron Amendment (#32) --- .../src/enums/definitions.json | 33 ++++++ .../src/enums/xahau-definitions-base.ts | 8 +- packages/xahau/HISTORY.md | 5 + .../xahau/src/models/ledger/AccountRoot.ts | 2 + .../xahau/src/models/transactions/cron.ts | 17 +++ .../xahau/src/models/transactions/cronSet.ts | 68 +++++++++++ .../xahau/src/models/transactions/index.ts | 2 + .../src/models/transactions/transaction.ts | 9 +- packages/xahau/src/models/utils/flags.ts | 2 + packages/xahau/test/models/cronSet.test.ts | 108 ++++++++++++++++++ 10 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 packages/xahau/src/models/transactions/cron.ts create mode 100644 packages/xahau/src/models/transactions/cronSet.ts create mode 100644 packages/xahau/test/models/cronSet.test.ts diff --git a/packages/xahau-binary-codec/src/enums/definitions.json b/packages/xahau-binary-codec/src/enums/definitions.json index 449b81e8..2dd2dc94 100644 --- a/packages/xahau-binary-codec/src/enums/definitions.json +++ b/packages/xahau-binary-codec/src/enums/definitions.json @@ -29,6 +29,7 @@ "LEDGER_ENTRY_TYPES": { "Invalid": -1, "AccountRoot": 97, + "Cron": 65, "DirectoryNode": 100, "RippleState": 114, "Ticket": 84, @@ -798,6 +799,26 @@ "type": "UInt32" } ], + [ + "RepeatCount", + { + "nth": 94, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "DelaySeconds", + { + "nth": 95, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "XahauActivationLgrSeq", { @@ -1488,6 +1509,16 @@ "type": "Hash256" } ], + [ + "Cron", + { + "nth": 95, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "Amount", { @@ -2835,6 +2866,8 @@ "URITokenBuy": 47, "URITokenCreateSellOffer": 48, "URITokenCancelSellOffer": 49, + "Cron": 92, + "CronSet": 93, "SetRemarks": 94, "Remit": 95, "GenesisMint": 96, diff --git a/packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts b/packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts index 4fe302ba..40dbad0c 100644 --- a/packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts +++ b/packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts @@ -74,7 +74,13 @@ class XrplDefinitionsBase { .filter(([_key, value]) => value >= 0) .map(([key, _value]) => key) - const ignoreList = ['EnableAmendment', 'SetFee', 'UNLModify', 'EmitFailure'] + const ignoreList = [ + 'EnableAmendment', + 'SetFee', + 'UNLModify', + 'EmitFailure', + 'Cron', + ] this.transactionMap = Object.assign( {}, ...Object.entries(enums.TRANSACTION_TYPES) diff --git a/packages/xahau/HISTORY.md b/packages/xahau/HISTORY.md index 03a44637..e39c9446 100644 --- a/packages/xahau/HISTORY.md +++ b/packages/xahau/HISTORY.md @@ -4,6 +4,11 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ## Unreleased Changes +### Added +* Support for Cron Amendment + +## 4.0.1 (2025-10-03) + ### Added * parseTransactionFlags as a utility function in the xrpl package to streamline transactions flags-to-map conversion * Support for XLS-77d Deep-Freeze amendment diff --git a/packages/xahau/src/models/ledger/AccountRoot.ts b/packages/xahau/src/models/ledger/AccountRoot.ts index acca80b0..ca743273 100644 --- a/packages/xahau/src/models/ledger/AccountRoot.ts +++ b/packages/xahau/src/models/ledger/AccountRoot.ts @@ -84,6 +84,8 @@ export default interface AccountRoot extends BaseLedgerEntry, HasPreviousTxnID { GovernanceMarks?: string AccountIndex?: number TouchCount?: number + /* The cron job that is associated with this account. */ + Cron?: string } /** diff --git a/packages/xahau/src/models/transactions/cron.ts b/packages/xahau/src/models/transactions/cron.ts new file mode 100644 index 00000000..24ec63b9 --- /dev/null +++ b/packages/xahau/src/models/transactions/cron.ts @@ -0,0 +1,17 @@ +import { BaseTransaction } from './common' + +/** + * Cron job to be executed. + * + * @category Pseudo Transaction Models + */ +export interface Cron extends BaseTransaction { + TransactionType: 'Cron' + /** + * The ledger index where this pseudo-transaction appears. + * This distinguishes the pseudo-transaction from other occurrences of the same change. + */ + LedgerSequence: number + /** The owner of the cron job. */ + Owner: string +} diff --git a/packages/xahau/src/models/transactions/cronSet.ts b/packages/xahau/src/models/transactions/cronSet.ts new file mode 100644 index 00000000..36920a23 --- /dev/null +++ b/packages/xahau/src/models/transactions/cronSet.ts @@ -0,0 +1,68 @@ +import { ValidationError } from '../../errors' + +import { BaseTransaction, validateBaseTransaction } from './common' +/** + * Transaction Flags for an CronSet Transaction. + * + * @category Transaction Flags + */ +export enum CronSetFlags { + /** + * If set, indicates that the user would like to unset the cron job. + */ + tfCronUnset = 0x00000001, +} + +/** + * CronSet is a transaction model that allows an account to set a cron job. + * + * @category Transaction Models + */ +export interface CronSet extends BaseTransaction { + TransactionType: 'CronSet' + Flags?: number | CronSetFlags + RepeatCount?: number + DelaySeconds?: number +} + +const MAX_REPEAT_COUNT = 256 +// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- seconds in a year +const MIN_DELAY_SECONDS = 365 * 24 * 60 * 60 + +/** + * Verify the form and type of an CronSet at runtime. + * + * @param tx - An CronSet Transaction. + * @throws When the CronSet is Malformed. + */ +export function validateCronSet(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Flags === CronSetFlags.tfCronUnset) { + if (tx.RepeatCount !== undefined || tx.DelaySeconds !== undefined) { + throw new ValidationError( + 'CronSet: RepeatCount and DelaySeconds must not be set when Flags is set to tfCronUnset', + ) + } + } + + if (tx.RepeatCount !== undefined && typeof tx.RepeatCount !== 'number') { + throw new ValidationError('CronSet: RepeatCount must be a number') + } + + if (tx.RepeatCount !== undefined && tx.RepeatCount > MAX_REPEAT_COUNT) { + throw new ValidationError( + `CronSet: RepeatCount must be less than ${MAX_REPEAT_COUNT}`, + ) + } + + if (tx.DelaySeconds !== undefined && typeof tx.DelaySeconds !== 'number') { + throw new ValidationError('CronSet: DelaySeconds must be a number') + } + + if (tx.DelaySeconds !== undefined && tx.DelaySeconds > MIN_DELAY_SECONDS) { + throw new ValidationError( + `CronSet: DelaySeconds must be less than ${MIN_DELAY_SECONDS}`, + ) + } +} diff --git a/packages/xahau/src/models/transactions/index.ts b/packages/xahau/src/models/transactions/index.ts index 3eab9bd5..9e96bae2 100644 --- a/packages/xahau/src/models/transactions/index.ts +++ b/packages/xahau/src/models/transactions/index.ts @@ -17,6 +17,8 @@ export { CheckCancel } from './checkCancel' export { CheckCash } from './checkCash' export { CheckCreate } from './checkCreate' export { ClaimReward, ClaimRewardFlags } from './claimReward' +export { Cron } from './cron' +export { CronSet, CronSetFlags } from './cronSet' export { DepositPreauth } from './depositPreauth' export { EscrowCancel } from './escrowCancel' export { EscrowCreate } from './escrowCreate' diff --git a/packages/xahau/src/models/transactions/transaction.ts b/packages/xahau/src/models/transactions/transaction.ts index d4e56fdd..cea1b30f 100644 --- a/packages/xahau/src/models/transactions/transaction.ts +++ b/packages/xahau/src/models/transactions/transaction.ts @@ -12,6 +12,8 @@ import { CheckCreate, validateCheckCreate } from './checkCreate' import { ClaimReward, validateClaimReward } from './claimReward' import { Clawback, validateClawback } from './clawback' import { BaseTransaction, isIssuedCurrency } from './common' +import { Cron } from './cron' +import { CronSet, validateCronSet } from './cronSet' import { DepositPreauth, validateDepositPreauth } from './depositPreauth' import { EnableAmendment } from './enableAmendment' import { EscrowCancel, validateEscrowCancel } from './escrowCancel' @@ -68,6 +70,7 @@ export type SubmittableTransaction = | CheckCreate | ClaimReward | Clawback + | CronSet | DepositPreauth | EscrowCancel | EscrowCreate @@ -98,7 +101,7 @@ export type SubmittableTransaction = * * @category Transaction Models */ -export type PseudoTransaction = EnableAmendment | SetFee | UNLModify +export type PseudoTransaction = Cron | EnableAmendment | SetFee | UNLModify /** * All transactions that can live on the XAHL @@ -210,6 +213,10 @@ export function validate(transaction: Record): void { validateClawback(tx) break + case 'CronSet': + validateCronSet(tx) + break + case 'DepositPreauth': validateDepositPreauth(tx) break diff --git a/packages/xahau/src/models/utils/flags.ts b/packages/xahau/src/models/utils/flags.ts index 9dbe795b..99608bd9 100644 --- a/packages/xahau/src/models/utils/flags.ts +++ b/packages/xahau/src/models/utils/flags.ts @@ -8,6 +8,7 @@ import { } from '../ledger/AccountRoot' import { AccountSetTfFlags } from '../transactions/accountSet' import { GlobalFlags } from '../transactions/common' +import { CronSetFlags } from '../transactions/cronSet' import { OfferCreateFlags } from '../transactions/offerCreate' import { PaymentFlags } from '../transactions/payment' import { PaymentChannelClaimFlags } from '../transactions/paymentChannelClaim' @@ -52,6 +53,7 @@ const txToFlag = { PaymentChannelClaim: PaymentChannelClaimFlags, Payment: PaymentFlags, TrustSet: TrustSetFlags, + CronSet: CronSetFlags, } /** diff --git a/packages/xahau/test/models/cronSet.test.ts b/packages/xahau/test/models/cronSet.test.ts new file mode 100644 index 00000000..92b0dd2b --- /dev/null +++ b/packages/xahau/test/models/cronSet.test.ts @@ -0,0 +1,108 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { + CronSetFlags, + validateCronSet, +} from '../../src/models/transactions/cronSet' + +/** + * CronSet Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('CronSet', function () { + it(`verifies valid CronSet`, function () { + let validCronSet = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Fee: '100', + RepeatCount: 256, + DelaySeconds: 365 * 24 * 60 * 60, + } as any + + assert.doesNotThrow(() => validateCronSet(validCronSet)) + assert.doesNotThrow(() => validate(validCronSet)) + + validCronSet = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Fee: '100', + Flags: CronSetFlags.tfCronUnset, + } as any + + assert.doesNotThrow(() => validateCronSet(validCronSet)) + assert.doesNotThrow(() => validate(validCronSet)) + + validCronSet = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Fee: '100', + Flags: { tfCronUnset: true }, + } as any + + assert.doesNotThrow(() => validateCronSet(validCronSet)) + assert.doesNotThrow(() => validate(validCronSet)) + }) + + it(`throws w/ invalid Delete Operation`, function () { + const invalidDeleteOperation = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + Flags: CronSetFlags.tfCronUnset, + RepeatCount: 1, + DelaySeconds: 1, + Fee: '100', + } as any + + assert.throws( + () => validateCronSet(invalidDeleteOperation), + ValidationError, + 'CronSet: RepeatCount and DelaySeconds must not be set when Flags is set to tfCronUnset', + ) + assert.throws( + () => validate(invalidDeleteOperation), + ValidationError, + 'CronSet: RepeatCount and DelaySeconds must not be set when Flags is set to tfCronUnset', + ) + }) + + it(`throws w/ invalid RepeatCount`, function () { + const invalidRepeatCount = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + RepeatCount: 257, + Fee: '100', + } as any + + assert.throws( + () => validateCronSet(invalidRepeatCount), + ValidationError, + 'CronSet: RepeatCount must be less than 256', + ) + assert.throws( + () => validate(invalidRepeatCount), + ValidationError, + 'CronSet: RepeatCount must be less than 256', + ) + }) + it(`throws w/ invalid DelaySeconds`, function () { + const invalidDelaySeconds = { + TransactionType: 'CronSet', + Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', + DelaySeconds: 365 * 24 * 60 * 60 + 1, + Fee: '100', + } as any + + assert.throws( + () => validateCronSet(invalidDelaySeconds), + ValidationError, + `CronSet: DelaySeconds must be less than ${365 * 24 * 60 * 60}`, + ) + assert.throws( + () => validate(invalidDelaySeconds), + ValidationError, + `CronSet: DelaySeconds must be less than ${365 * 24 * 60 * 60}`, + ) + }) +})