From 8bbc84057ce69e3ceba685b4acb5120afab17fed Mon Sep 17 00:00:00 2001 From: tequ Date: Wed, 12 Nov 2025 20:01:33 +0900 Subject: [PATCH] Support Cron StartTime, improvements (#33) --- .../src/enums/definitions.json | 10 +++++ packages/xahau/src/models/ledger/Cron.ts | 25 +++++++++++ .../xahau/src/models/ledger/LedgerEntry.ts | 3 ++ packages/xahau/src/models/ledger/index.ts | 2 + .../xahau/src/models/methods/ledgerEntry.ts | 14 ++++++ .../xahau/src/models/transactions/cronSet.ts | 45 ++++++++++++++----- packages/xahau/src/utils/hashes/index.ts | 24 ++++++++++ .../xahau/src/utils/hashes/ledgerSpaces.ts | 1 + packages/xahau/test/models/cronSet.test.ts | 21 +++++++-- packages/xahau/test/utils/hashes.test.ts | 10 +++++ 10 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 packages/xahau/src/models/ledger/Cron.ts diff --git a/packages/xahau-binary-codec/src/enums/definitions.json b/packages/xahau-binary-codec/src/enums/definitions.json index 2dd2dc94..0f8ad010 100644 --- a/packages/xahau-binary-codec/src/enums/definitions.json +++ b/packages/xahau-binary-codec/src/enums/definitions.json @@ -799,6 +799,16 @@ "type": "UInt32" } ], + [ + "StartTime", + { + "nth": 93, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "RepeatCount", { diff --git a/packages/xahau/src/models/ledger/Cron.ts b/packages/xahau/src/models/ledger/Cron.ts new file mode 100644 index 00000000..28c89a88 --- /dev/null +++ b/packages/xahau/src/models/ledger/Cron.ts @@ -0,0 +1,25 @@ +import { Account } from '../transactions/common' + +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' + +/** + * The EmittedTxn object type contains the + * + * @category Ledger Entries + */ +export default interface Cron extends BaseLedgerEntry, HasPreviousTxnID { + LedgerEntryType: 'Cron' + /** The owner of the cron job. */ + Owner: Account + /** The start time of the cron job. */ + StartTime: number + /** The delay seconds of the cron job. */ + DelaySeconds: number + /** The repeat count of the cron job. */ + RepeatCount: number + /** + * A hint indicating which page of the sender's owner directory links to this + * object, in case the directory consists of multiple pages. + */ + OwnerNode: string +} diff --git a/packages/xahau/src/models/ledger/LedgerEntry.ts b/packages/xahau/src/models/ledger/LedgerEntry.ts index 74c93986..b804b1aa 100644 --- a/packages/xahau/src/models/ledger/LedgerEntry.ts +++ b/packages/xahau/src/models/ledger/LedgerEntry.ts @@ -1,6 +1,7 @@ import AccountRoot from './AccountRoot' import Amendments from './Amendments' import Check from './Check' +import Cron from './Cron' import DepositPreauth from './DepositPreauth' import DirectoryNode from './DirectoryNode' import EmittedTxn from './EmittedTxn' @@ -23,6 +24,7 @@ import URIToken from './URIToken' type LedgerEntry = | AccountRoot | Amendments + | Cron | Check | DepositPreauth | DirectoryNode @@ -46,6 +48,7 @@ type LedgerEntry = type LedgerEntryFilter = | 'account' | 'amendments' + | 'cron' | 'check' | 'deposit_preauth' | 'directory' diff --git a/packages/xahau/src/models/ledger/index.ts b/packages/xahau/src/models/ledger/index.ts index 51a667fc..31b928e2 100644 --- a/packages/xahau/src/models/ledger/index.ts +++ b/packages/xahau/src/models/ledger/index.ts @@ -4,6 +4,7 @@ import AccountRoot, { } from './AccountRoot' import Amendments, { Majority, AMENDMENTS_ID } from './Amendments' import Check from './Check' +import Cron from './Cron' import DepositPreauth from './DepositPreauth' import DirectoryNode from './DirectoryNode' import EmittedTxn from './EmittedTxn' @@ -36,6 +37,7 @@ export { AMENDMENTS_ID, Amendments, Check, + Cron, DepositPreauth, DirectoryNode, EmittedTxn, diff --git a/packages/xahau/src/models/methods/ledgerEntry.ts b/packages/xahau/src/models/methods/ledgerEntry.ts index 03096c9a..c3ebfc36 100644 --- a/packages/xahau/src/models/methods/ledgerEntry.ts +++ b/packages/xahau/src/models/methods/ledgerEntry.ts @@ -200,6 +200,20 @@ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest { uri: string } | string + + /** + * The Cron object to retrieve. If a string, must be the object ID of the + * Cron, as hexadecimal. If an object, the `owner` and `time` + * sub-fields are required to uniquely specify the Cron entry. + */ + cron?: + | { + /** The owner of the Cron object. */ + owner: string + /** The start time of the Cron object. */ + time: number + } + | string } /** diff --git a/packages/xahau/src/models/transactions/cronSet.ts b/packages/xahau/src/models/transactions/cronSet.ts index 36920a23..9ac43de5 100644 --- a/packages/xahau/src/models/transactions/cronSet.ts +++ b/packages/xahau/src/models/transactions/cronSet.ts @@ -1,6 +1,12 @@ import { ValidationError } from '../../errors' -import { BaseTransaction, validateBaseTransaction } from './common' +import { + BaseTransaction, + isNumber, + validateBaseTransaction, + validateOptionalField, + validateRequiredField, +} from './common' /** * Transaction Flags for an CronSet Transaction. * @@ -23,6 +29,7 @@ export interface CronSet extends BaseTransaction { Flags?: number | CronSetFlags RepeatCount?: number DelaySeconds?: number + StartTime?: number } const MAX_REPEAT_COUNT = 256 @@ -38,29 +45,43 @@ const MIN_DELAY_SECONDS = 365 * 24 * 60 * 60 export function validateCronSet(tx: Record): void { validateBaseTransaction(tx) - if (tx.Flags === CronSetFlags.tfCronUnset) { - if (tx.RepeatCount !== undefined || tx.DelaySeconds !== undefined) { + if ( + typeof tx.Flags === 'number' && + // eslint-disable-next-line no-bitwise -- bitwise operation to check if the flag is set + tx.Flags & CronSetFlags.tfCronUnset + ) { + if ( + tx.RepeatCount !== undefined || + tx.DelaySeconds !== undefined || + tx.StartTime !== undefined + ) { throw new ValidationError( - 'CronSet: RepeatCount and DelaySeconds must not be set when Flags is set to tfCronUnset', + 'CronSet: RepeatCount, DelaySeconds, and StartTime must not be set when Flags is set to tfCronUnset', ) } + return } - if (tx.RepeatCount !== undefined && typeof tx.RepeatCount !== 'number') { - throw new ValidationError('CronSet: RepeatCount must be a number') + validateRequiredField(tx, 'StartTime', isNumber) + validateOptionalField(tx, 'RepeatCount', isNumber) + validateOptionalField(tx, 'DelaySeconds', isNumber) + + if ((tx.RepeatCount === undefined) !== (tx.DelaySeconds === undefined)) { + throw new ValidationError( + 'CronSet: Both RepeatCount and DelaySeconds must be set, or neither should be set', + ) } - if (tx.RepeatCount !== undefined && tx.RepeatCount > MAX_REPEAT_COUNT) { + if (typeof tx.RepeatCount === 'number' && 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) { + if ( + typeof tx.DelaySeconds === 'number' && + tx.DelaySeconds > MIN_DELAY_SECONDS + ) { throw new ValidationError( `CronSet: DelaySeconds must be less than ${MIN_DELAY_SECONDS}`, ) diff --git a/packages/xahau/src/utils/hashes/index.ts b/packages/xahau/src/utils/hashes/index.ts index 5267554d..8efac942 100644 --- a/packages/xahau/src/utils/hashes/index.ts +++ b/packages/xahau/src/utils/hashes/index.ts @@ -199,4 +199,28 @@ export function hashURIToken(issuer: string, uri: string): string { ) } +/** + * Compute the Hash of a Cron LedgerEntry. + * + * @param owner - Account of the Cron. + * @param time - Time of the Cron. + * @returns Hash of the Cron. + * @category Utilities + */ +export function hashCron(owner: string, time: number): string { + const timeString = bytesToHex([ + (time >> 24) & 0xff, + (time >> 16) & 0xff, + (time >> 8) & 0xff, + (time >> 0) & 0xff, + ]) + + const nsHash = sha512Half(ledgerSpaceHex('cron')).slice(0, 16) + const accHash = sha512Half( + ledgerSpaceHex('cron') + timeString + addressToHex(owner), + ).slice(0, 40) + + return nsHash + timeString + accHash +} + export { hashLedgerHeader, hashSignedTx, hashLedger, hashStateTree, hashTxTree } diff --git a/packages/xahau/src/utils/hashes/ledgerSpaces.ts b/packages/xahau/src/utils/hashes/ledgerSpaces.ts index 56c62ef8..4d7a1364 100644 --- a/packages/xahau/src/utils/hashes/ledgerSpaces.ts +++ b/packages/xahau/src/utils/hashes/ledgerSpaces.ts @@ -30,6 +30,7 @@ const ledgerSpaces = { check: 'C', depositPreauth: 'p', uriToken: 'U', + cron: 'L', } export default ledgerSpaces diff --git a/packages/xahau/test/models/cronSet.test.ts b/packages/xahau/test/models/cronSet.test.ts index 92b0dd2b..673e7513 100644 --- a/packages/xahau/test/models/cronSet.test.ts +++ b/packages/xahau/test/models/cronSet.test.ts @@ -1,6 +1,10 @@ import { assert } from 'chai' -import { validate, ValidationError } from '../../src' +import { + setTransactionFlagsToNumber, + validate, + ValidationError, +} from '../../src' import { CronSetFlags, validateCronSet, @@ -19,6 +23,7 @@ describe('CronSet', function () { Fee: '100', RepeatCount: 256, DelaySeconds: 365 * 24 * 60 * 60, + StartTime: 0, } as any assert.doesNotThrow(() => validateCronSet(validCronSet)) @@ -41,7 +46,10 @@ describe('CronSet', function () { Flags: { tfCronUnset: true }, } as any - assert.doesNotThrow(() => validateCronSet(validCronSet)) + assert.doesNotThrow(() => { + setTransactionFlagsToNumber(validCronSet) + validateCronSet(validCronSet) + }) assert.doesNotThrow(() => validate(validCronSet)) }) @@ -52,18 +60,19 @@ describe('CronSet', function () { Flags: CronSetFlags.tfCronUnset, RepeatCount: 1, DelaySeconds: 1, + StartTime: 1, Fee: '100', } as any assert.throws( () => validateCronSet(invalidDeleteOperation), ValidationError, - 'CronSet: RepeatCount and DelaySeconds must not be set when Flags is set to tfCronUnset', + 'CronSet: RepeatCount, DelaySeconds, and StartTime 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', + 'CronSet: RepeatCount, DelaySeconds, and StartTime must not be set when Flags is set to tfCronUnset', ) }) @@ -72,6 +81,8 @@ describe('CronSet', function () { TransactionType: 'CronSet', Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', RepeatCount: 257, + StartTime: 1, + DelaySeconds: 1, Fee: '100', } as any @@ -91,6 +102,8 @@ describe('CronSet', function () { TransactionType: 'CronSet', Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', DelaySeconds: 365 * 24 * 60 * 60 + 1, + StartTime: 1, + RepeatCount: 1, Fee: '100', } as any diff --git a/packages/xahau/test/utils/hashes.test.ts b/packages/xahau/test/utils/hashes.test.ts index df38417b..44f9ebec 100644 --- a/packages/xahau/test/utils/hashes.test.ts +++ b/packages/xahau/test/utils/hashes.test.ts @@ -20,6 +20,7 @@ import { hashAccountRoot, hashOfferId, hashSignerListId, + hashCron, } from '../../src/utils/hashes' import fixtures from '../fixtures/xahaud' import { assertResultMatch } from '../testUtils' @@ -148,6 +149,15 @@ describe('Hashes', function () { assert.equal(actualEntryHash, expectedEntryHash) }) + it('calcCronEntryHash', function () { + const owner = 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn' + const time = 30758410 + const expectedEntryHash = + 'F7B645436187CC6101D5560AF1127C15262825333ADC45B3155918D98149BD3F' + const actualEntryHash = hashCron(owner, time) + assert.equal(actualEntryHash, expectedEntryHash) + }) + it('Hash a signed transaction correctly', function () { const expected_hash = '458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2'