Compare commits

...

10 Commits

Author SHA1 Message Date
Denis Angell
35c294218d cut v4.0.3 2025-11-18 12:29:03 +01:00
tequ
58150f156e generate correct xahau-latest-min.js file name (#3034) (#39) 2025-11-18 12:15:20 +01:00
Denis Angell
d7b3a02ad2 cut v4.0.2 2025-11-12 12:31:40 +01:00
tequ
ac33a1584d Support ExtendedHookState (#34) 2025-11-12 12:01:45 +01:00
tequ
8bbc84057c Support Cron StartTime, improvements (#33) 2025-11-12 12:01:33 +01:00
tequ
0ca36e1314 Update xahauci 2025.7.9 (#36) 2025-11-12 12:01:16 +01:00
tequ
e454c61994 Support Cron Amendment (#32) 2025-10-17 18:44:07 +10:00
Denis Angell
b5f15ac075 cut v4.0.1 2025-10-02 19:09:24 +02:00
tequ
5ec5ad8e1e Deep Freeze XLS-77d (#2873) (#29)
Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
Co-authored-by: Denis Angell <dangell@transia.co>
2025-10-02 18:51:18 +02:00
tequ
043620b637 feat: add Clawback amendment support (#2353) (#28)
* Add Clawback transaction
* Account flag lsfAllowTrustLineClawback
* Support bitwise flag checking of 64 bit flags

Co-authored-by: Shawn Xie <35279399+shawnxie999@users.noreply.github.com>
2025-10-02 18:50:42 +02:00
40 changed files with 898 additions and 49 deletions

View File

@@ -63,11 +63,6 @@ online_delete=256
[debug_logfile]
/var/log/rippled/debug.log
[sntp_servers]
time.windows.com
time.apple.com
time.nist.gov
pool.ntp.org
[ips]
r.ripple.com 51235
@@ -167,6 +162,8 @@ fixXahauV1
fixXahauV2
fixXahauV3
PaychanAndEscrowForTokens
DeepFreeze
Clawback
[network_id]
21337

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
XAHAUD_DOCKER_IMAGE: xahauci/xahaud:2025.2.6
XAHAUD_DOCKER_IMAGE: xahauci/xahaud:2025.7.9
on:
push:

View File

@@ -226,47 +226,48 @@ This should almost always be done using the [`xrpl-codec-gen`](https://github.co
## Release
1. Checkout `main` (or your beta branch) and `git pull`.
1. Create a new branch (`git checkout -b <BRANCH_NAME>`) to capture updates that take place during this process.
1. Update `HISTORY.md` to reflect release changes.
2. Create a new branch (`git checkout -b <BRANCH_NAME>`) to capture updates that take place during this process.
3. Update `HISTORY.md` to reflect release changes.
- [ ] Update the version number and release date, and ensure it lists the changes since the previous release.
1. Run `npm run docgen` if the docs were modified in this release to update them (skip this step for a beta).
1. Run `npm run build` to triple check the build still works
1. Run `npx lerna version --no-git-tag-version` - This bumps the package versions.
4. Run `npm run docgen` if the docs were modified in this release to update them (skip this step for a beta).
5. Run `npm run clean` to delete previously generated artifacts.
6. Run `npm run build` to triple check the build still works
7. Run `npx lerna version --no-git-tag-version` - This bumps the package versions.
- For each changed package, pick what the new version should be. Lerna will bump the versions, commit version bumps to `main`, and create a new git tag for each published package.
- If you do NOT want to update the package number, choose "Custom Version" and set the version to be the same as the existing version. Lerna will not publish any changes in this case.
- If publishing a beta, make sure that the versions are all of the form `a.b.c-beta.d`, where `a`, `b`, and `c` are identical to the last normal release except for one, which has been incremented by 1.
1. Run `npm i` to update the package-lock with the updated versions.
1. Create a new PR from this branch into `main` and merge it (you can directly merge into the beta branch for a beta).
1. Checkout `main` and `git pull` (you can skip this step for a beta since you already have the latest version of the beta branch).
1. Actually publish the packages with one of the following:
8. Run `npm i` to update the package-lock with the updated versions.
9. Create a new PR from this branch into `main` and merge it (you can directly merge into the beta branch for a beta).
10. Checkout `main` and `git pull` (you can skip this step for a beta since you already have the latest version of the beta branch).
11. Actually publish the packages with one of the following:
- Stable release: Run `npx lerna publish from-package --yes`
- Beta release: Run `npx lerna publish from-package --dist-tag beta --yes`
Notice this allows developers to install the package with `npm add xahau@beta`
- Stable release: Run `npx lerna publish from-package --yes`
- Beta release: Run `npx lerna publish from-package --dist-tag beta --yes`
Notice this allows developers to install the package with `npm add xahau@beta`
1. If requested, enter your [npmjs.com](https://npmjs.com) OTP (one-time password) to complete publication.
12. If requested, enter your [npmjs.com](https://npmjs.com) OTP (one-time password) to complete publication.
NOW YOU HAVE PUBLISHED! But you're not done; we have to notify people!
NOW YOU HAVE PUBLISHED! But you're not done; we have to notify people!
1. Run `git tag <tagname> -m <tagname>`, where `<tagname>` is the new package and version (e.g. `xahau@2.1.1`), for each version released.
1. Run `git push --follow-tags`, to push the tags to Github.
1. On GitHub, click the "Releases" link on the right-hand side of the page.
13. Run `git tag <tagname> -m <tagname>`, where `<tagname>` is the new package and version (e.g. `xahau@2.1.1`), for each version released.
14. Run `git push --follow-tags`, to push the tags to Github.
15. On GitHub, click the "Releases" link on the right-hand side of the page.
1. Repeat for each release:
16. Repeat for each release:
1. Click "Draft a new release"
1. Click "Choose a tag", and choose a tag that you just created.
1. Edit the name of the release to match the tag (IE \<package\>@\<version\>) and edit the description as you see fit.
1. Click "Draft a new release"
2. Click "Choose a tag", and choose a tag that you just created.
3. Edit the name of the release to match the tag (IE \<package\>@\<version\>) and edit the description as you see fit.
1. Send an email to [xahau-announce](https://groups.google.com/g/xahau-announce).
1. Lastly, send a similar message to the Xahau Discord in the [`javascript` channel](https://discord.com/channels/1085202760548499486/1085203623111295068). The message should include:
17. Send an email to [xahau-announce](https://groups.google.com/g/xahau-announce).
18. Lastly, send a similar message to the Xahau Discord in the [`javascript` channel](https://discord.com/channels/1085202760548499486/1085203623111295068). The message should include:
1. The version changes for xahau libraries
1. A link to the more detailed changes
1. Highlights of important changes
2. A link to the more detailed changes
3. Highlights of important changes
## Mailing Lists

8
command.sh Executable file
View File

@@ -0,0 +1,8 @@
for file in $(git log --diff-filter=D --name-only --format="" | grep -E "oracle.*\.ts$"); do
commit=$(git rev-list -n 1 HEAD -- "$file")
if [ ! -z "$commit" ]; then
git checkout "$commit~1" -- "$file"
echo "restore: $file"
fi
done
rsync -av packages/xrpl/ packages/xahau/ && rm -rf packages/xrpl/

6
package-lock.json generated
View File

@@ -14746,7 +14746,7 @@
}
},
"packages/xahau": {
"version": "4.0.0",
"version": "4.0.2",
"license": "ISC",
"dependencies": {
"@scure/bip32": "^1.3.1",
@@ -14756,7 +14756,7 @@
"bignumber.js": "^9.0.0",
"eventemitter3": "^5.0.1",
"xahau-address-codec": "^5.0.0",
"xahau-binary-codec": "^2.1.0",
"xahau-binary-codec": "^2.1.2",
"xahau-keypairs": "^2.0.0"
},
"devDependencies": {
@@ -14789,7 +14789,7 @@
}
},
"packages/xahau-binary-codec": {
"version": "2.1.0",
"version": "2.1.2",
"license": "ISC",
"dependencies": {
"@xrplf/isomorphic": "^1.0.1",

View File

@@ -4,6 +4,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## Unreleased
### BREAKING CHANGES:
* Renamed `build/xrplf-secret-numbers-latest.min.js` to `build/xrplf-secret-numbers-latest-min.js`.
## 1.0.0 (2024-02-01)
### BREAKING CHANGES:

View File

@@ -8,7 +8,7 @@
"types": "dist/index.d.ts",
"scripts": {
"prepublish": "npm run clean && npm run lint && npm run test && npm run test:browser && npm run build",
"clean": "rm -rf ./dist ./coverage ./test/testCompiledForWeb tsconfig.build.tsbuildinfo",
"clean": "rm -rf ./build ./dist ./coverage ./test/testCompiledForWeb tsconfig.build.tsbuildinfo",
"test": "jest --verbose",
"test:browser": "npm run build && npm run build:browserTests && karma start ./karma.config.js",
"build": "run-s build:lib build:web",

View File

@@ -1,6 +1,6 @@
{
"name": "xahau-binary-codec",
"version": "2.1.0",
"version": "2.1.3",
"description": "XAH Ledger binary codec",
"files": [
"dist/*",

View File

@@ -29,6 +29,7 @@
"LEDGER_ENTRY_TYPES": {
"Invalid": -1,
"AccountRoot": 97,
"Cron": 65,
"DirectoryNode": 100,
"RippleState": 114,
"Ticket": 84,
@@ -328,6 +329,16 @@
"type": "UInt16"
}
],
[
"HookStateScale",
{
"nth": 21,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt16"
}
],
[
"NetworkID",
{
@@ -798,6 +809,36 @@
"type": "UInt32"
}
],
[
"StartTime",
{
"nth": 93,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"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 +1529,16 @@
"type": "Hash256"
}
],
[
"Cron",
{
"nth": 95,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Hash256"
}
],
[
"Amount",
{
@@ -2797,6 +2848,7 @@
"tecINSUF_RESERVE_SELLER": 187,
"tecIMMUTABLE": 188,
"tecTOO_MANY_REMARKS": 189,
"tecHAS_HOOK_STATE": 190,
"tecLAST_POSSIBLE_ENTRY": 255
},
"TRANSACTION_TYPES": {
@@ -2829,11 +2881,14 @@
"NFTokenCreateOffer": 27,
"NFTokenCancelOffer": 28,
"NFTokenAcceptOffer": 29,
"Clawback": 30,
"URITokenMint": 45,
"URITokenBurn": 46,
"URITokenBuy": 47,
"URITokenCreateSellOffer": 48,
"URITokenCancelSellOffer": 49,
"Cron": 92,
"CronSet": 93,
"SetRemarks": 94,
"Remit": 95,
"GenesisMint": 96,

View File

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

View File

@@ -3,9 +3,20 @@
Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release.
## Unreleased Changes
### Added
* Fixed minified `build/xahau-latest-min.js` to have all the latest xahau package changes.
## 4.0.1 (2025-11-12)
### Added
* Support for Cron Amendment
* Support for ExtendedHookState 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
### Fixed
* `TransactionStream` model supports APIv2

View File

@@ -1,6 +1,6 @@
{
"name": "xahau",
"version": "4.0.0",
"version": "4.0.3",
"license": "ISC",
"description": "A TypeScript/JavaScript API for interacting with the XAH Ledger in Node.js and the browser",
"files": [
@@ -29,7 +29,7 @@
"bignumber.js": "^9.0.0",
"eventemitter3": "^5.0.1",
"xahau-address-codec": "^5.0.0",
"xahau-binary-codec": "^2.1.0",
"xahau-binary-codec": "^2.1.3",
"xahau-keypairs": "^2.0.0"
},
"devDependencies": {
@@ -56,7 +56,7 @@
"build:browserTests": "webpack --config ./test/webpack.config.js",
"analyze": "webpack --analyze",
"watch": "run-s build:lib --watch",
"clean": "rm -rf ./dist ./coverage ./test/testCompiledForWeb tsconfig.build.tsbuildinfo",
"clean": "rm -rf ./build ./dist ./coverage ./test/testCompiledForWeb tsconfig.build.tsbuildinfo",
"docgen": "tsc --build tsconfig.docs.json && typedoc && echo js.xahau.org >> ../../docs/CNAME",
"prepare": "copyfiles ../../README.md xahau/README.md",
"prepublish": "run-s clean build",

View File

@@ -84,6 +84,9 @@ export default interface AccountRoot extends BaseLedgerEntry, HasPreviousTxnID {
GovernanceMarks?: string
AccountIndex?: number
TouchCount?: number
HookStateScale?: number
/* The cron job that is associated with this account. */
Cron?: string
}
/**
@@ -152,6 +155,11 @@ export interface AccountRootFlagsInterface {
* Disallow incoming Remit from other accounts.
*/
lsfDisallowIncomingRemit?: boolean
/**
* This address can claw back issued IOUs. Once enabled, cannot be disabled.
*/
lsfAllowTrustLineClawback?: boolean
}
export enum AccountRootFlags {
@@ -216,4 +224,8 @@ export enum AccountRootFlags {
* Disallow incoming Remits from other accounts.
*/
lsfDisallowIncomingRemit = 0x80000000,
/**
* This address can claw back issued IOUs. Once enabled, cannot be disabled.
*/
lsfAllowTrustLineClawback = 0x00001000,
}

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 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'

View File

@@ -77,4 +77,8 @@ export enum RippleStateFlags {
lsfHighFreeze = 0x00800000,
// True, trust line to AMM. Used by client apps to identify payments via AMM.
lsfAMMNode = 0x01000000,
// True, low side has set deep freeze flag
lsfLowDeepFreeze = 0x02000000,
// True, high side has set deep freeze flag
lsfHighDeepFreeze = 0x04000000,
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import {
Account,
BaseTransaction,
isAccount,
isNumber,
validateBaseTransaction,
validateOptionalField,
} from './common'
@@ -61,6 +62,8 @@ export enum AccountSetAsfFlags {
asfDisallowIncomingTrustline = 15,
/** Disallow other accounts from sending incoming Remits */
asfDisallowIncomingRemit = 16,
/** Permanently gain the ability to claw back issued IOUs */
asfAllowTrustLineClawback = 17,
}
/**
@@ -161,10 +164,15 @@ export interface AccountSet extends BaseTransaction {
* account's behalf using NFTokenMint's `Issuer` field.
*/
NFTokenMinter?: Account
/**
* The allowed scale of the hook state.
*/
HookStateScale?: number
}
const MIN_TICK_SIZE = 3
const MAX_TICK_SIZE = 15
const MAX_HOOK_STATE_SCALE = 16
/**
* Verify the form and type of an AccountSet at runtime.
@@ -172,7 +180,7 @@ const MAX_TICK_SIZE = 15
* @param tx - An AccountSet Transaction.
* @throws When the AccountSet is Malformed.
*/
// eslint-disable-next-line max-lines-per-function -- okay for this method, only a little over
// eslint-disable-next-line max-lines-per-function, max-statements -- okay for this method, only a little over
export function validateAccountSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
@@ -223,4 +231,14 @@ export function validateAccountSet(tx: Record<string, unknown>): void {
throw new ValidationError('AccountSet: invalid TickSize')
}
}
validateOptionalField(tx, 'HookStateScale', isNumber)
if (
typeof tx.HookStateScale === 'number' &&
tx.HookStateScale > MAX_HOOK_STATE_SCALE
) {
throw new ValidationError(
`AccountSet: HookStateScale must be less than ${MAX_HOOK_STATE_SCALE}`,
)
}
}

View File

@@ -0,0 +1,49 @@
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount } from '../common'
import {
BaseTransaction,
validateBaseTransaction,
isIssuedCurrency,
} from './common'
/**
* The Clawback transaction is used by the token issuer to claw back
* issued tokens from a holder.
*/
export interface Clawback extends BaseTransaction {
TransactionType: 'Clawback'
/**
* Indicates the AccountID that submitted this transaction. The account MUST
* be the issuer of the currency.
*/
Account: string
/**
* The amount of currency to deliver, and it must be non-XRP. The nested field
* names MUST be lower-case. The `issuer` field MUST be the holder's address,
* whom to be clawed back.
*/
Amount: IssuedCurrencyAmount
}
/**
* Verify the form and type of an Clawback at runtime.
*
* @param tx - An Clawback Transaction.
* @throws When the Clawback is Malformed.
*/
export function validateClawback(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.Amount == null) {
throw new ValidationError('Clawback: missing field Amount')
}
if (!isIssuedCurrency(tx.Amount)) {
throw new ValidationError('Clawback: invalid Amount')
}
if (isIssuedCurrency(tx.Amount) && tx.Account === tx.Amount.issuer) {
throw new ValidationError('Clawback: invalid holder Account')
}
}

View File

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

View File

@@ -0,0 +1,89 @@
import { ValidationError } from '../../errors'
import {
BaseTransaction,
isNumber,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} 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
StartTime?: 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<string, unknown>): void {
validateBaseTransaction(tx)
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, DelaySeconds, and StartTime must not be set when Flags is set to tfCronUnset',
)
}
return
}
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 (typeof tx.RepeatCount === 'number' && tx.RepeatCount > MAX_REPEAT_COUNT) {
throw new ValidationError(
`CronSet: RepeatCount must be less than ${MAX_REPEAT_COUNT}`,
)
}
if (
typeof tx.DelaySeconds === 'number' &&
tx.DelaySeconds > MIN_DELAY_SECONDS
) {
throw new ValidationError(
`CronSet: DelaySeconds must be less than ${MIN_DELAY_SECONDS}`,
)
}
}

View File

@@ -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'
@@ -61,3 +63,4 @@ export { URITokenCreateSellOffer } from './uriTokenCreateSellOffer'
export { URITokenBuy } from './uriTokenBuy'
export { URITokenCancelSellOffer } from './uriTokenCancelSellOffer'
export { UNLModify } from './UNLModify'
export { Clawback } from './clawback'

View File

@@ -10,7 +10,10 @@ import { CheckCancel, validateCheckCancel } from './checkCancel'
import { CheckCash, validateCheckCash } from './checkCash'
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'
@@ -66,6 +69,8 @@ export type SubmittableTransaction =
| CheckCash
| CheckCreate
| ClaimReward
| Clawback
| CronSet
| DepositPreauth
| EscrowCancel
| EscrowCreate
@@ -96,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
@@ -204,6 +209,14 @@ export function validate(transaction: Record<string, unknown>): void {
validateClaimReward(tx)
break
case 'Clawback':
validateClawback(tx)
break
case 'CronSet':
validateCronSet(tx)
break
case 'DepositPreauth':
validateDepositPreauth(tx)
break

View File

@@ -30,6 +30,11 @@ export enum TrustSetFlags {
tfSetFreeze = 0x00100000,
/** Unfreeze the trust line. */
tfClearFreeze = 0x00200000,
/** Deep-Freeze the trustline -- disallow sending and receiving the said IssuedCurrency */
/** Allowed only if the trustline is already regularly frozen, or if tfSetFreeze is set in the same transaction. */
tfSetDeepFreeze = 0x00400000,
/** Clear a Deep-Frozen trustline */
tfClearDeepFreeze = 0x00800000,
}
/**
@@ -89,6 +94,11 @@ export interface TrustSetFlagsInterface extends GlobalFlags {
tfSetFreeze?: boolean
/** Unfreeze the trust line. */
tfClearFreeze?: boolean
/** Deep-Freeze the trustline -- disallow sending and receiving the said IssuedCurrency */
/** Allowed only if the trustline is already regularly frozen, or if tfSetFreeze is set in the same transaction. */
tfSetDeepFreeze?: boolean
/** Clear a Deep-Frozen trust line */
tfClearDeepFreeze?: boolean
}
/**

View File

@@ -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,
}
/**

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 }

View File

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

View File

@@ -117,6 +117,7 @@ describe('server_info (xahaud)', function () {
'time',
'uptime',
'complete_ledgers',
'complete_ledgers_pinned',
'hostid',
'load',
'state_accounting',

View File

@@ -105,6 +105,7 @@ describe('server_state', function () {
const removeKeys = [
'complete_ledgers',
'complete_ledgers_pinned',
'load',
'state_accounting',
'pubkey_node',

View File

@@ -0,0 +1,115 @@
import { assert } from 'chai'
import {
AccountSet,
AccountSetAsfFlags,
TrustSet,
Payment,
Clawback,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { generateFundedWallet, testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('Clawback', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const setupAccountSetTx: AccountSet = {
TransactionType: 'AccountSet',
Account: testContext.wallet.classicAddress,
SetFlag: AccountSetAsfFlags.asfAllowTrustLineClawback,
}
await testTransaction(
testContext.client,
setupAccountSetTx,
testContext.wallet,
)
const setupTrustSetTx: TrustSet = {
TransactionType: 'TrustSet',
Account: wallet2.classicAddress,
LimitAmount: {
currency: 'USD',
issuer: testContext.wallet.classicAddress,
value: '1000',
},
}
await testTransaction(testContext.client, setupTrustSetTx, wallet2)
const setupPaymentTx: Payment = {
TransactionType: 'Payment',
Account: testContext.wallet.classicAddress,
Destination: wallet2.classicAddress,
Amount: {
currency: 'USD',
issuer: testContext.wallet.classicAddress,
value: '1000',
},
}
await testTransaction(
testContext.client,
setupPaymentTx,
testContext.wallet,
)
// verify that line is created
const objectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'state',
})
assert.lengthOf(
objectsResponse.result.account_objects,
1,
'Should be exactly one line on the ledger',
)
// actual test - clawback
const tx: Clawback = {
TransactionType: 'Clawback',
Account: testContext.wallet.classicAddress,
Amount: {
currency: 'USD',
issuer: wallet2.classicAddress,
value: '500',
},
}
await testTransaction(testContext.client, tx, testContext.wallet)
// verify amount clawed back
const linesResponse = await testContext.client.request({
command: 'account_lines',
account: wallet2.classicAddress,
})
assert.lengthOf(
linesResponse.result.lines,
1,
'Should be exactly one line on the ledger',
)
assert.equal(
'500',
linesResponse.result.lines[0].balance,
`Holder balance incorrect after Clawback`,
)
},
TIMEOUT,
)
})

View File

@@ -1,24 +1,36 @@
import { assert } from 'chai'
import { OfferCreate } from '../../../src'
import { OfferCreate, TrustSet, Wallet } from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
import {
testTransaction,
generateFundedWallet,
submitTransaction,
} from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('OfferCreate', function () {
let testContext: XrplIntegrationTestContext
let wallet_deep_freeze_trustline: Wallet | undefined
beforeEach(async () => {
beforeAll(async () => {
testContext = await setupClient(serverUrl)
if (!wallet_deep_freeze_trustline) {
// eslint-disable-next-line require-atomic-updates -- race condition doesn't really matter
wallet_deep_freeze_trustline = await generateFundedWallet(
testContext.client,
)
}
})
afterEach(async () => teardownClient(testContext))
afterAll(async () => teardownClient(testContext))
it(
'base',
@@ -49,4 +61,52 @@ describe('OfferCreate', function () {
},
TIMEOUT,
)
it(
'OfferCreate with Deep-Frozen trustline must fail',
async () => {
assert(wallet_deep_freeze_trustline != null)
// deep-freeze the trust line
const trust_set_tx: TrustSet = {
TransactionType: 'TrustSet',
Account: testContext.wallet.classicAddress,
LimitAmount: {
currency: 'USD',
issuer: wallet_deep_freeze_trustline.classicAddress,
value: '10',
},
Flags: {
tfSetFreeze: true,
tfSetDeepFreeze: true,
},
}
await testTransaction(
testContext.client,
trust_set_tx,
testContext.wallet,
)
const offer_create_tx: OfferCreate = {
TransactionType: 'OfferCreate',
Account: testContext.wallet.classicAddress,
TakerGets: '13100000',
TakerPays: {
currency: 'USD',
issuer: wallet_deep_freeze_trustline.classicAddress,
value: '10',
},
}
const response = await submitTransaction({
client: testContext.client,
transaction: offer_create_tx,
wallet: testContext.wallet,
})
assert.equal(response.result.engine_result, 'tecFROZEN')
},
TIMEOUT,
)
})

View File

@@ -1,6 +1,8 @@
import { assert } from 'chai'
import { TrustSet, percentToQuality, Wallet } from '../../../src'
import { RippleState } from '../../../src/models/ledger/index'
import { RippleStateFlags } from '../../../src/models/ledger/RippleState'
import serverUrl from '../serverUrl'
import {
setupClient,
@@ -85,4 +87,60 @@ describe('TrustSet', function () {
},
TIMEOUT,
)
it(
'Create a Deep-Frozen trustline',
async () => {
assert(wallet2 != null)
// deep-freeze a trustline with the specified counter-party/currency-code
const tx: TrustSet = {
TransactionType: 'TrustSet',
Account: testContext.wallet.classicAddress,
LimitAmount: {
currency: 'USD',
issuer: wallet2.classicAddress,
value: '10',
},
Flags: {
tfSetFreeze: true,
tfSetDeepFreeze: true,
},
}
const response = await testTransaction(
testContext.client,
tx,
testContext.wallet,
)
assert.equal(response.result.engine_result, 'tesSUCCESS')
// assert that the trustline is frozen
const trustLine = await testContext.client.request({
command: 'account_lines',
account: testContext.wallet.classicAddress,
})
assert.equal(trustLine.result.lines[0].freeze, true)
// verify that the trust-line is deep-frozen
// this operation cannot be done with the account_lines RPC
const account_objects = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
})
const rippleState = account_objects.result
.account_objects[0] as RippleState
// Depending on the pseudo-random generation of accounts,
// either of the below leger-object flags must be set
const hasDeepFreeze =
// eslint-disable-next-line no-bitwise -- required to validate flag
(rippleState.Flags & RippleStateFlags.lsfHighDeepFreeze) |
// eslint-disable-next-line no-bitwise -- required to validate flag
(rippleState.Flags & RippleStateFlags.lsfLowDeepFreeze)
assert.isTrue(hasDeepFreeze !== 0)
},
TIMEOUT,
)
})

View File

@@ -163,4 +163,31 @@ describe('AccountSet', function () {
'AccountSet: invalid field NFTokenMinter',
)
})
it(`throws w/ invalid HookStateScale`, function () {
account.HookStateScale = ''
assert.throws(
() => validateAccountSet(account),
ValidationError,
'AccountSet: invalid field HookStateScale',
)
assert.throws(
() => validate(account),
ValidationError,
'AccountSet: invalid field HookStateScale',
)
account.HookStateScale = 17
assert.throws(
() => validateAccountSet(account),
ValidationError,
'AccountSet: HookStateScale must be less than 16',
)
assert.throws(
() => validate(account),
ValidationError,
'AccountSet: HookStateScale must be less than 16',
)
})
})

View File

@@ -0,0 +1,81 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
/**
* Clawback Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('Clawback', function () {
it(`verifies valid Clawback`, function () {
const validClawback = {
TransactionType: 'Clawback',
Amount: {
currency: 'DSH',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '43.11584856965009',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.doesNotThrow(() => validate(validClawback))
})
it(`throws w/ missing Amount`, function () {
const missingAmount = {
TransactionType: 'Clawback',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(missingAmount),
ValidationError,
'Clawback: missing field Amount',
)
})
it(`throws w/ invalid Amount`, function () {
const invalidAmount = {
TransactionType: 'Clawback',
Amount: 100000000,
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAmount),
ValidationError,
'Clawback: invalid Amount',
)
const invalidStrAmount = {
TransactionType: 'Clawback',
Amount: '1234',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidStrAmount),
ValidationError,
'Clawback: invalid Amount',
)
})
it(`throws w/ invalid holder Account`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
currency: 'DSH',
issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
value: '43.11584856965009',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: invalid holder Account',
)
})
})

View File

@@ -0,0 +1,121 @@
import { assert } from 'chai'
import {
setTransactionFlagsToNumber,
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,
StartTime: 0,
} 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(() => {
setTransactionFlagsToNumber(validCronSet)
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,
StartTime: 1,
Fee: '100',
} as any
assert.throws(
() => validateCronSet(invalidDeleteOperation),
ValidationError,
'CronSet: RepeatCount, DelaySeconds, and StartTime must not be set when Flags is set to tfCronUnset',
)
assert.throws(
() => validate(invalidDeleteOperation),
ValidationError,
'CronSet: RepeatCount, DelaySeconds, and StartTime must not be set when Flags is set to tfCronUnset',
)
})
it(`throws w/ invalid RepeatCount`, function () {
const invalidRepeatCount = {
TransactionType: 'CronSet',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
RepeatCount: 257,
StartTime: 1,
DelaySeconds: 1,
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,
StartTime: 1,
RepeatCount: 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}`,
)
})
})

View File

@@ -22,6 +22,11 @@ describe('TrustSet', function () {
},
QualityIn: 1234,
QualityOut: 4321,
// an example of deep-frozen trustline
Flags: {
tfSetFreeze: true,
tfSetDeepFreeze: true,
},
} as any
})

View File

@@ -168,7 +168,8 @@ describe('Models Utils', function () {
AccountRootFlags.lsfDisallowIncomingCheck |
AccountRootFlags.lsfDisallowIncomingPayChan |
AccountRootFlags.lsfDisallowIncomingTrustline |
AccountRootFlags.lsfDisallowIncomingRemit
AccountRootFlags.lsfDisallowIncomingRemit |
AccountRootFlags.lsfAllowTrustLineClawback
const parsed = parseAccountRootFlags(accountRootFlags)
@@ -186,7 +187,8 @@ describe('Models Utils', function () {
parsed.lsfDisallowIncomingCheck &&
parsed.lsfDisallowIncomingPayChan &&
parsed.lsfDisallowIncomingTrustline &&
parsed.lsfDisallowIncomingRemit,
parsed.lsfDisallowIncomingRemit &&
parsed.lsfAllowTrustLineClawback,
)
})
@@ -207,6 +209,7 @@ describe('Models Utils', function () {
assert.isUndefined(parsed.lsfDisallowIncomingPayChan)
assert.isUndefined(parsed.lsfDisallowIncomingTrustline)
assert.isUndefined(parsed.lsfDisallowIncomingRemit)
assert.isUndefined(parsed.lsfAllowTrustLineClawback)
})
it('parseTransactionFlags all enabled', function () {

View File

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

View File

@@ -48,7 +48,7 @@ module.exports = {
const localConfig = merge(config, {
mode: "production",
output: {
filename: `${filename}-latest.min.js`,
filename: `${filename}-latest-min.js`,
},
});