mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-12-01 09:35:48 +00:00
Support Cron StartTime, improvements (#33)
This commit is contained in:
@@ -799,6 +799,16 @@
|
|||||||
"type": "UInt32"
|
"type": "UInt32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"StartTime",
|
||||||
|
{
|
||||||
|
"nth": 93,
|
||||||
|
"isVLEncoded": false,
|
||||||
|
"isSerialized": true,
|
||||||
|
"isSigningField": true,
|
||||||
|
"type": "UInt32"
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"RepeatCount",
|
"RepeatCount",
|
||||||
{
|
{
|
||||||
|
|||||||
25
packages/xahau/src/models/ledger/Cron.ts
Normal file
25
packages/xahau/src/models/ledger/Cron.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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}`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user