Support Cron StartTime, improvements (#33)

This commit is contained in:
tequ
2025-11-12 20:01:33 +09:00
committed by GitHub
parent 0ca36e1314
commit 8bbc84057c
10 changed files with 139 additions and 16 deletions

View File

@@ -799,6 +799,16 @@
"type": "UInt32" "type": "UInt32"
} }
], ],
[
"StartTime",
{
"nth": 93,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt32"
}
],
[ [
"RepeatCount", "RepeatCount",
{ {

View File

@@ -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
}

View File

@@ -1,6 +1,7 @@
import AccountRoot from './AccountRoot' import AccountRoot from './AccountRoot'
import Amendments from './Amendments' import Amendments from './Amendments'
import Check from './Check' import Check from './Check'
import Cron from './Cron'
import DepositPreauth from './DepositPreauth' import DepositPreauth from './DepositPreauth'
import DirectoryNode from './DirectoryNode' import DirectoryNode from './DirectoryNode'
import EmittedTxn from './EmittedTxn' import EmittedTxn from './EmittedTxn'
@@ -23,6 +24,7 @@ import URIToken from './URIToken'
type LedgerEntry = type LedgerEntry =
| AccountRoot | AccountRoot
| Amendments | Amendments
| Cron
| Check | Check
| DepositPreauth | DepositPreauth
| DirectoryNode | DirectoryNode
@@ -46,6 +48,7 @@ type LedgerEntry =
type LedgerEntryFilter = type LedgerEntryFilter =
| 'account' | 'account'
| 'amendments' | 'amendments'
| 'cron'
| 'check' | 'check'
| 'deposit_preauth' | 'deposit_preauth'
| 'directory' | 'directory'

View File

@@ -4,6 +4,7 @@ import AccountRoot, {
} from './AccountRoot' } from './AccountRoot'
import Amendments, { Majority, AMENDMENTS_ID } from './Amendments' import Amendments, { Majority, AMENDMENTS_ID } from './Amendments'
import Check from './Check' import Check from './Check'
import Cron from './Cron'
import DepositPreauth from './DepositPreauth' import DepositPreauth from './DepositPreauth'
import DirectoryNode from './DirectoryNode' import DirectoryNode from './DirectoryNode'
import EmittedTxn from './EmittedTxn' import EmittedTxn from './EmittedTxn'
@@ -36,6 +37,7 @@ export {
AMENDMENTS_ID, AMENDMENTS_ID,
Amendments, Amendments,
Check, Check,
Cron,
DepositPreauth, DepositPreauth,
DirectoryNode, DirectoryNode,
EmittedTxn, EmittedTxn,

View File

@@ -200,6 +200,20 @@ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
uri: string uri: string
} }
| 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
} }
/** /**

View File

@@ -1,6 +1,12 @@
import { ValidationError } from '../../errors' import { ValidationError } from '../../errors'
import { BaseTransaction, validateBaseTransaction } from './common' import {
BaseTransaction,
isNumber,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/** /**
* Transaction Flags for an CronSet Transaction. * Transaction Flags for an CronSet Transaction.
* *
@@ -23,6 +29,7 @@ export interface CronSet extends BaseTransaction {
Flags?: number | CronSetFlags Flags?: number | CronSetFlags
RepeatCount?: number RepeatCount?: number
DelaySeconds?: number DelaySeconds?: number
StartTime?: number
} }
const MAX_REPEAT_COUNT = 256 const MAX_REPEAT_COUNT = 256
@@ -38,29 +45,43 @@ const MIN_DELAY_SECONDS = 365 * 24 * 60 * 60
export function validateCronSet(tx: Record<string, unknown>): void { export function validateCronSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx) validateBaseTransaction(tx)
if (tx.Flags === CronSetFlags.tfCronUnset) { if (
if (tx.RepeatCount !== undefined || tx.DelaySeconds !== undefined) { 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( 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') { validateRequiredField(tx, 'StartTime', isNumber)
throw new ValidationError('CronSet: RepeatCount must be a number') 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( throw new ValidationError(
`CronSet: RepeatCount must be less than ${MAX_REPEAT_COUNT}`, `CronSet: RepeatCount must be less than ${MAX_REPEAT_COUNT}`,
) )
} }
if (tx.DelaySeconds !== undefined && typeof tx.DelaySeconds !== 'number') { if (
throw new ValidationError('CronSet: DelaySeconds must be a number') typeof tx.DelaySeconds === 'number' &&
} tx.DelaySeconds > MIN_DELAY_SECONDS
) {
if (tx.DelaySeconds !== undefined && tx.DelaySeconds > MIN_DELAY_SECONDS) {
throw new ValidationError( throw new ValidationError(
`CronSet: DelaySeconds must be less than ${MIN_DELAY_SECONDS}`, `CronSet: DelaySeconds must be less than ${MIN_DELAY_SECONDS}`,
) )

View File

@@ -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 } export { hashLedgerHeader, hashSignedTx, hashLedger, hashStateTree, hashTxTree }

View File

@@ -30,6 +30,7 @@ const ledgerSpaces = {
check: 'C', check: 'C',
depositPreauth: 'p', depositPreauth: 'p',
uriToken: 'U', uriToken: 'U',
cron: 'L',
} }
export default ledgerSpaces export default ledgerSpaces

View File

@@ -1,6 +1,10 @@
import { assert } from 'chai' import { assert } from 'chai'
import { validate, ValidationError } from '../../src' import {
setTransactionFlagsToNumber,
validate,
ValidationError,
} from '../../src'
import { import {
CronSetFlags, CronSetFlags,
validateCronSet, validateCronSet,
@@ -19,6 +23,7 @@ describe('CronSet', function () {
Fee: '100', Fee: '100',
RepeatCount: 256, RepeatCount: 256,
DelaySeconds: 365 * 24 * 60 * 60, DelaySeconds: 365 * 24 * 60 * 60,
StartTime: 0,
} as any } as any
assert.doesNotThrow(() => validateCronSet(validCronSet)) assert.doesNotThrow(() => validateCronSet(validCronSet))
@@ -41,7 +46,10 @@ describe('CronSet', function () {
Flags: { tfCronUnset: true }, Flags: { tfCronUnset: true },
} as any } as any
assert.doesNotThrow(() => validateCronSet(validCronSet)) assert.doesNotThrow(() => {
setTransactionFlagsToNumber(validCronSet)
validateCronSet(validCronSet)
})
assert.doesNotThrow(() => validate(validCronSet)) assert.doesNotThrow(() => validate(validCronSet))
}) })
@@ -52,18 +60,19 @@ describe('CronSet', function () {
Flags: CronSetFlags.tfCronUnset, Flags: CronSetFlags.tfCronUnset,
RepeatCount: 1, RepeatCount: 1,
DelaySeconds: 1, DelaySeconds: 1,
StartTime: 1,
Fee: '100', Fee: '100',
} as any } as any
assert.throws( assert.throws(
() => validateCronSet(invalidDeleteOperation), () => validateCronSet(invalidDeleteOperation),
ValidationError, 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( assert.throws(
() => validate(invalidDeleteOperation), () => validate(invalidDeleteOperation),
ValidationError, 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', TransactionType: 'CronSet',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
RepeatCount: 257, RepeatCount: 257,
StartTime: 1,
DelaySeconds: 1,
Fee: '100', Fee: '100',
} as any } as any
@@ -91,6 +102,8 @@ describe('CronSet', function () {
TransactionType: 'CronSet', TransactionType: 'CronSet',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo', Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
DelaySeconds: 365 * 24 * 60 * 60 + 1, DelaySeconds: 365 * 24 * 60 * 60 + 1,
StartTime: 1,
RepeatCount: 1,
Fee: '100', Fee: '100',
} as any } as any

View File

@@ -20,6 +20,7 @@ import {
hashAccountRoot, hashAccountRoot,
hashOfferId, hashOfferId,
hashSignerListId, hashSignerListId,
hashCron,
} from '../../src/utils/hashes' } from '../../src/utils/hashes'
import fixtures from '../fixtures/xahaud' import fixtures from '../fixtures/xahaud'
import { assertResultMatch } from '../testUtils' import { assertResultMatch } from '../testUtils'
@@ -148,6 +149,15 @@ describe('Hashes', function () {
assert.equal(actualEntryHash, expectedEntryHash) 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 () { it('Hash a signed transaction correctly', function () {
const expected_hash = const expected_hash =
'458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2' '458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2'