Compare commits

..

13 Commits

Author SHA1 Message Date
Omar Khan
5b16d0cfe3 4.2.0 release (#2896)
* update HISTORY

* update package versions

* fix version
2025-02-13 21:18:57 -05:00
Omar Khan
35e40d9d71 feat: add AMMClawback support (#2893)
* update definitions and package.json

* update rippled.cfg

* add AMMClawback transaction with unit test

* switch to rippled v2.3.1

* Revert "switch to rippled v2.3.1"

This reverts commit d0bd3bdae97fc82240d5dd18732fa301255dc26a.

* update definitions

* add integ test

* resolve type assertions

* remove package-lock.json in sub packages

* remove ts-expect-error

* update integ test

* console logs

* fix console logs

* add tfClawTwoAssets test and remove logs

* fix lint error

* refactor type assertions

* refactor error messages

* final cleanup
2025-02-11 19:00:51 -05:00
Chenna Keshava B S
ea9e3dcc98 Deep Freeze XLS-77d (#2873) 2025-02-11 12:43:32 -08:00
dependabot[bot]
61da4c567a build(deps-dev): bump @types/lodash from 4.17.13 to 4.17.15 (#2891) 2025-02-10 17:04:47 +00:00
Chenna Keshava B S
189abc1a26 PermissionedDomain XLS-80d (#2874) 2025-02-07 14:44:53 -08:00
Mayukha Vadari
ce5ca316ca feat: add support for the simulate RPC (XLS-69d) (#2867) 2025-02-07 10:38:52 -08:00
yinyiqian1
991a1d29a4 Add more unit test for DynamicNFT (#2892)
* Add more unit test for DynamicNFT

* resolve comment
2025-02-04 22:56:01 -05:00
yinyiqian1
23d26c8c2e support DynamicNFT (#2726)
* support DynamicNFT

* Update history.md

* use xrpl-codec-gen to generate definitions.json and modify ripple-binary-code HISTORY.md

* use validateRequiredField for NFTokenID check

* move comment to a proper place

* Add some comment and modify integration test

* update transaction number

* update ci rippled version
2025-02-03 10:32:30 -05:00
achowdhry-ripple
abdb192c69 Fix: parseTransactionFlags unintentionally modifies transaction (#2825)
* remove setTransactions and any reference to modifying a transaction parameter

* remove use of setter in autofill

* changelog and test fixes

* add back deprecated function with warnings

* add new helper function to exported

* update readme with deprecated

* remove references to deprecated setter

* fix changelog syntax

* revert to test old functions

* lint

* lint for deprecation

* remove unsafe assertions

* separate null check

* clean up tests

* remove outdated logic

* fix history md

* fix tests after merge conflicts

* Update packages/xrpl/HISTORY.md

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/src/models/utils/flags.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* Update packages/xrpl/test/models/utils.test.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

* rename a test per pr comment

* lint fixes

* Update packages/xrpl/test/models/utils.test.ts

Co-authored-by: Omar Khan <khancodegt@gmail.com>

---------

Co-authored-by: Omar Khan <khancodegt@gmail.com>
2025-01-16 11:59:44 -05:00
dependabot[bot]
84943ae0b6 build(deps-dev): bump ts-jest from 29.2.2 to 29.2.5 (#2764) 2025-01-02 15:45:13 +00:00
dependabot[bot]
d8126807a4 build(deps-dev): bump typedoc from 0.26.11 to 0.27.6 (#2863) 2025-01-02 15:29:56 +00:00
dependabot[bot]
7a2fa3fcaa build(deps-dev): bump webpack from 5.96.1 to 5.97.1 (#2853) 2025-01-02 15:23:03 +00:00
dependabot[bot]
76c3355858 build(deps-dev): bump webpack-cli from 5.1.4 to 6.0.1 (#2861) 2025-01-02 15:14:29 +00:00
50 changed files with 1994 additions and 739 deletions

View File

@@ -63,17 +63,10 @@ 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
[validators_file]
validators.txt
[rpc_startup]
{ "command": "log_level", "severity": "info" }
@@ -180,6 +173,7 @@ fixXChainRewardRounding
fixPreviousTxnID
fixAMMv1_1
# 2.3.0 Amendments
AMMClawback
fixAMMv1_2
Credentials
NFTokenMintOffer
@@ -188,3 +182,6 @@ fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2
DeepFreeze
DynamicNFT
PermissionedDomains

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
RIPPLED_DOCKER_IMAGE: rippleci/rippled:develop
on:
push:

View File

@@ -64,7 +64,7 @@ From the top-level xrpl.js folder (one level above `packages`), run the followin
```bash
npm install
# sets up the rippled standalone Docker container - you can skip this step if you already have it set up
docker run -p 6006:6006 --rm -it --name rippled_standalone --volume $PWD/.ci-config:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:2.3.0-rc1 -c 'rippled -a'
docker run -p 6006:6006 --rm -it --name rippled_standalone --volume $PWD/.ci-config:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:develop -c 'rippled -a'
npm run build
npm run test:integration
```
@@ -76,7 +76,7 @@ Breaking down the command:
`--name rippled_standalone` is an instance name for clarity
* `--volume $PWD/.ci-config:/etc/opt/ripple/` identifies the `rippled.cfg` and `validators.txt` to import. It must be an absolute path, so we use `$PWD` instead of `./`.
* `rippleci/rippled` is an image that is regularly updated with the latest `rippled` releases
* `--entrypoint bash rippleci/rippled:2.3.0-rc1` manually overrides the entrypoint (for versions of rippled >= 2.3.0)
* `--entrypoint bash rippleci/rippled:develop` manually overrides the entrypoint (for the latest version of rippled on the `develop` branch)
* `-c 'rippled -a'` provides the bash command to start `rippled` in standalone mode from the manual entrypoint
### Browser Tests
@@ -92,7 +92,7 @@ This should be run from the `xrpl.js` top level folder (one above the `packages`
```bash
npm run build
# sets up the rippled standalone Docker container - you can skip this step if you already have it set up
docker run -p 6006:6006 --rm -it --name rippled_standalone --volume $PWD/.ci-config:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:2.3.0-rc1 -c 'rippled -a'
docker run -p 6006:6006 --rm -it --name rippled_standalone --volume $PWD/.ci-config:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:develop -c 'rippled -a'
npm run test:browser
```

817
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@
"typescript": "^5.1.6",
"webpack": "^5.81.0",
"webpack-bundle-analyzer": "^4.1.0",
"webpack-cli": "^5.0.1"
"webpack-cli": "^6.0.1"
},
"workspaces": [
"./packages/*"

View File

@@ -2,6 +2,12 @@
## Unreleased
## 2.3.0 (2025-2-13)
### Added
* Support for the AMMClawback amendment (XLS-73)
* Support for the Permissioned Domains amendment (XLS-80).
## 2.2.0 (2024-12-23)
### Added
@@ -11,6 +17,7 @@
### Added
* Support for the Price Oracles amendment (XLS-47).
* Support for the `DynamicNFT` amendment (XLS-46)
### Fixed
* Better error handling/error messages for serialization/deserialization errors.

View File

@@ -1,6 +1,6 @@
{
"name": "ripple-binary-codec",
"version": "2.2.0",
"version": "2.3.0",
"description": "XRP Ledger binary codec",
"files": [
"dist/*",

View File

@@ -1250,6 +1250,16 @@
"type": "Hash256"
}
],
[
"DomainID",
{
"nth": 34,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "Hash256"
}
],
[
"hash",
{
@@ -2530,6 +2540,15 @@
"type": "STArray"
}
],
[
"AcceptedCredentials", {
"nth": 28,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STArray"
}
],
[
"CloseResolution",
{
@@ -2863,6 +2882,7 @@
"Oracle": 128,
"Credential": 129,
"PayChannel": 120,
"PermissionedDomain": 130,
"RippleState": 114,
"SignerList": 83,
"Ticket": 84,
@@ -3053,6 +3073,7 @@
},
"TRANSACTION_TYPES": {
"AMMBid": 39,
"AMMClawback": 31,
"AMMCreate": 35,
"AMMDelete": 40,
"AMMDeposit": 36,
@@ -3085,6 +3106,7 @@
"NFTokenCancelOffer": 28,
"NFTokenCreateOffer": 27,
"NFTokenMint": 25,
"NFTokenModify": 61,
"OfferCancel": 8,
"OfferCreate": 7,
"OracleDelete": 52,
@@ -3093,6 +3115,8 @@
"PaymentChannelClaim": 15,
"PaymentChannelCreate": 13,
"PaymentChannelFund": 14,
"PermissionedDomainSet": 62,
"PermissionedDomainDelete": 63,
"SetFee": 101,
"SetRegularKey": 5,
"SignerListSet": 12,

View File

@@ -2,7 +2,22 @@
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
## Unreleased
## 4.2.0 (2025-2-13)
### Added
* Support for the AMMClawback amendment (XLS-73)
* Adds utility function `convertTxFlagsToNumber`
* Support for the Permissioned Domains amendment (XLS-80).
* Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
* Support for XLS-77d Deep-Freeze amendment
### Changed
* Deprecated `setTransactionFlagsToNumber`. Start using convertTxFlagsToNumber instead
### Fixed
* Include `network_id` field in the `server_state` response interface.
## 4.1.0 (2024-12-23)
@@ -11,6 +26,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
* New `MPTAmount` type support for `Payment` and `Clawback` transactions
* `parseTransactionFlags` as a utility function in the xrpl package to streamline transactions flags-to-map conversion
* Support for XLS-70d (Credentials)
* Support for the `DynamicNFT` amendment (XLS-46)
### Fixed
* `TransactionStream` model supports APIv2

View File

@@ -1,6 +1,6 @@
{
"name": "xrpl",
"version": "4.1.0",
"version": "4.2.0",
"license": "ISC",
"description": "A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser",
"files": [
@@ -29,7 +29,7 @@
"bignumber.js": "^9.0.0",
"eventemitter3": "^5.0.1",
"ripple-address-codec": "^5.0.0",
"ripple-binary-codec": "^2.2.0",
"ripple-binary-codec": "^2.3.0",
"ripple-keypairs": "^2.0.0"
},
"devDependencies": {
@@ -43,7 +43,7 @@
"lodash": "^4.17.4",
"react": "^19.0.0",
"run-s": "^0.0.0",
"typedoc": "0.26.11",
"typedoc": "0.27.6",
"ws": "^8.14.2"
},
"resolutions": {

View File

@@ -40,14 +40,19 @@ import type {
MarkerRequest,
MarkerResponse,
SubmitResponse,
SimulateRequest,
} from '../models/methods'
import type { BookOffer, BookOfferCurrency } from '../models/methods/bookOffers'
import {
SimulateBinaryResponse,
SimulateJsonResponse,
} from '../models/methods/simulate'
import type {
EventTypes,
OnEventToListenerMap,
} from '../models/methods/subscribe'
import type { SubmittableTransaction } from '../models/transactions'
import { setTransactionFlagsToNumber } from '../models/utils/flags'
import { convertTxFlagsToNumber } from '../models/utils/flags'
import {
ensureClassicAddress,
submitRequest,
@@ -665,7 +670,7 @@ class Client extends EventEmitter<EventTypes> {
const tx = { ...transaction }
setValidAddresses(tx)
setTransactionFlagsToNumber(tx)
tx.Flags = convertTxFlagsToNumber(tx)
const promises: Array<Promise<void>> = []
if (tx.NetworkID == null) {
@@ -764,6 +769,41 @@ class Client extends EventEmitter<EventTypes> {
return submitRequest(this, signedTx, opts?.failHard)
}
/**
* Simulates an unsigned transaction.
* Steps performed on a transaction:
* 1. Autofill.
* 2. Sign & Encode.
* 3. Submit.
*
* @category Core
*
* @param transaction - A transaction to autofill, sign & encode, and submit.
* @param opts - (Optional) Options used to sign and submit a transaction.
* @param opts.binary - If true, return the metadata in a binary encoding.
*
* @returns A promise that contains SimulateResponse.
* @throws RippledError if the simulate request fails.
*/
public async simulate<Binary extends boolean = false>(
transaction: SubmittableTransaction | string,
opts?: {
// If true, return the binary-encoded representation of the results.
binary?: Binary
},
): Promise<
Binary extends true ? SimulateBinaryResponse : SimulateJsonResponse
> {
// send request
const binary = opts?.binary ?? false
const request: SimulateRequest =
typeof transaction === 'string'
? { command: 'simulate', tx_blob: transaction, binary }
: { command: 'simulate', tx_json: transaction, binary }
return this.request(request)
}
/**
* Asynchronously submits a transaction and verifies that it has been included in a
* validated ledger (or has errored/will not be included for some reason).

View File

@@ -8,8 +8,9 @@
*/
export * as LedgerEntry from './ledger'
export {
setTransactionFlagsToNumber,
parseAccountRootFlags,
setTransactionFlagsToNumber,
convertTxFlagsToNumber,
parseTransactionFlags,
} from './utils/flags'
export * from './methods'

View File

@@ -13,6 +13,7 @@ import NegativeUNL from './NegativeUNL'
import Offer from './Offer'
import Oracle from './Oracle'
import PayChannel from './PayChannel'
import PermissionedDomain from './PermissionedDomain'
import RippleState from './RippleState'
import SignerList from './SignerList'
import Ticket from './Ticket'
@@ -35,6 +36,7 @@ type LedgerEntry =
| Offer
| Oracle
| PayChannel
| PermissionedDomain
| RippleState
| SignerList
| Ticket
@@ -61,6 +63,7 @@ type LedgerEntryFilter =
| 'offer'
| 'oracle'
| 'payment_channel'
| 'permissioned_domain'
| 'signer_list'
| 'state'
| 'ticket'

View File

@@ -0,0 +1,29 @@
import { AuthorizeCredential } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export default interface PermissionedDomain
extends BaseLedgerEntry,
HasPreviousTxnID {
/* The ledger object's type (PermissionedDomain). */
LedgerEntryType: 'PermissionedDomain'
/* The account that controls the settings of the domain. */
Owner: string
/* The credentials that are accepted by the domain.
Ownership of one of these credentials automatically
makes you a member of the domain. */
AcceptedCredentials: AuthorizeCredential[]
/* Flag values associated with this object. */
Flags: 0
/* Owner account's directory page containing the PermissionedDomain object. */
OwnerNode: string
/* The Sequence value of the PermissionedDomainSet
transaction that created this domain. Used in combination
with the Account to identify this domain. */
Sequence: number
}

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

@@ -148,6 +148,14 @@ import {
StateAccountingFinal,
} from './serverInfo'
import { ServerStateRequest, ServerStateResponse } from './serverState'
import {
SimulateBinaryRequest,
SimulateBinaryResponse,
SimulateJsonRequest,
SimulateJsonResponse,
SimulateRequest,
SimulateResponse,
} from './simulate'
import { SubmitRequest, SubmitResponse } from './submit'
import {
SubmitMultisignedRequest,
@@ -203,6 +211,7 @@ type Request =
| LedgerDataRequest
| LedgerEntryRequest
// transaction methods
| SimulateRequest
| SubmitRequest
| SubmitMultisignedRequest
| TransactionEntryRequest
@@ -261,6 +270,7 @@ type Response<Version extends APIVersion = typeof DEFAULT_API_VERSION> =
| LedgerDataResponse
| LedgerEntryResponse
// transaction methods
| SimulateResponse
| SubmitResponse
| SubmitMultisignedVersionResponseMap<Version>
| TransactionEntryResponse
@@ -398,6 +408,12 @@ export type RequestResponseMap<
? LedgerDataResponse
: T extends LedgerEntryRequest
? LedgerEntryResponse
: T extends SimulateBinaryRequest
? SimulateBinaryResponse
: T extends SimulateJsonRequest
? SimulateJsonResponse
: T extends SimulateRequest
? SimulateJsonResponse
: T extends SubmitRequest
? SubmitResponse
: T extends SubmitMultisignedRequest
@@ -544,6 +560,8 @@ export {
LedgerEntryRequest,
LedgerEntryResponse,
// transaction methods with types
SimulateRequest,
SimulateResponse,
SubmitRequest,
SubmitResponse,
SubmitMultisignedRequest,

View File

@@ -203,13 +203,13 @@ export interface LedgerQueueData {
}
export interface LedgerBinary
extends Omit<Omit<Ledger, 'transactions'>, 'accountState'> {
extends Omit<Ledger, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}
export interface LedgerBinaryV1
extends Omit<Omit<LedgerV1, 'transactions'>, 'accountState'> {
extends Omit<LedgerV1, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}

View File

@@ -51,6 +51,7 @@ export interface ServerStateResponse extends BaseResponse {
load_factor_fee_queue?: number
load_factor_fee_reference?: number
load_factor_server?: number
network_id: number
peer_disconnects?: string
peer_disconnects_resources?: string
peers: number

View File

@@ -0,0 +1,88 @@
import {
BaseTransaction,
Transaction,
TransactionMetadata,
} from '../transactions'
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `simulate` method simulates a transaction without submitting it to the network.
* Returns a {@link SimulateResponse}.
*
* @category Requests
*/
export type SimulateRequest = BaseRequest & {
command: 'simulate'
binary?: boolean
} & (
| {
tx_blob: string
tx_json?: never
}
| {
tx_json: Transaction
tx_blob?: never
}
)
export type SimulateBinaryRequest = SimulateRequest & {
binary: true
}
export type SimulateJsonRequest = SimulateRequest & {
binary?: false
}
/**
* Response expected from an {@link SimulateRequest}.
*
* @category Responses
*/
export type SimulateResponse = SimulateJsonResponse | SimulateBinaryResponse
export interface SimulateBinaryResponse extends BaseResponse {
result: {
applied: false
engine_result: string
engine_result_code: number
engine_result_message: string
tx_blob: string
meta_blob: string
/**
* The ledger index of the ledger version that was used to generate this
* response.
*/
ledger_index: number
}
}
export interface SimulateJsonResponse<T extends BaseTransaction = Transaction>
extends BaseResponse {
result: {
applied: false
engine_result: string
engine_result_code: number
engine_result_message: string
/**
* The ledger index of the ledger version that was used to generate this
* response.
*/
ledger_index: number
tx_json: T
meta?: TransactionMetadata<T>
}
}

View File

@@ -0,0 +1,120 @@
import { ValidationError } from '../../errors'
import { Currency, IssuedCurrency, IssuedCurrencyAmount } from '../common'
import {
Account,
BaseTransaction,
GlobalFlags,
isAccount,
isAmount,
isCurrency,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* Enum representing values for AMMClawback Transaction Flags.
*
* @category Transaction Flags
*/
export enum AMMClawbackFlags {
tfClawTwoAssets = 0x00000001,
}
/**
* Map of flags to boolean values representing {@link AMMClawback} transaction
* flags.
*
* @category Transaction Flags
*/
export interface AMMClawbackFlagsInterface extends GlobalFlags {
tfClawTwoAssets?: boolean
}
/**
* Claw back tokens from a holder that has deposited your issued tokens into an AMM pool.
*
* Clawback is disabled by default. To use clawback, you must send an AccountSet transaction to enable the
* Allow Trust Line Clawback setting. An issuer with any existing tokens cannot enable clawback. You can
* only enable Allow Trust Line Clawback if you have a completely empty owner directory, meaning you must
* do so before you set up any trust lines, offers, escrows, payment channels, checks, or signer lists.
* After you enable clawback, it cannot reverted: the account permanently gains the ability to claw back
* issued assets on trust lines.
*/
export interface AMMClawback extends BaseTransaction {
TransactionType: 'AMMClawback'
/**
* The account holding the asset to be clawed back.
*/
Holder: Account
/**
* Specifies the asset that the issuer wants to claw back from the AMM pool.
* In JSON, this is an object with currency and issuer fields. The issuer field must match with Account.
*/
Asset: IssuedCurrency
/**
* Specifies the other asset in the AMM's pool. In JSON, this is an object with currency and
* issuer fields (omit issuer for XRP).
*/
Asset2: Currency
/**
* The maximum amount to claw back from the AMM account. The currency and issuer subfields should match
* the Asset subfields. If this field isn't specified, or the value subfield exceeds the holder's available
* tokens in the AMM, all of the holder's tokens will be clawed back.
*/
Amount?: IssuedCurrencyAmount
}
/**
* Verify the form and type of an AMMClawback at runtime.
*
* @param tx - An AMMClawback Transaction.
* @throws {ValidationError} When the transaction is malformed.
*/
export function validateAMMClawback(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'Holder', isAccount)
validateRequiredField(tx, 'Asset', isCurrency)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- required
const asset = tx.Asset as IssuedCurrency
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- required
const amount = tx.Amount as IssuedCurrencyAmount
if (tx.Holder === asset.issuer) {
throw new ValidationError(
'AMMClawback: Holder and Asset.issuer must be distinct',
)
}
if (tx.Account !== asset.issuer) {
throw new ValidationError(
'AMMClawback: Account must be the same as Asset.issuer',
)
}
validateRequiredField(tx, 'Asset2', isCurrency)
validateOptionalField(tx, 'Amount', isAmount)
if (tx.Amount != null) {
if (amount.currency !== asset.currency) {
throw new ValidationError(
'AMMClawback: Amount.currency must match Asset.currency',
)
}
if (amount.issuer !== asset.issuer) {
throw new ValidationError(
'AMMClawback: Amount.issuer must match Amount.issuer',
)
}
}
}

View File

@@ -38,6 +38,10 @@ export enum NFTokenMintFlags {
* issuer.
*/
tfTransferable = 0x00000008,
/**
* If set, indicates that this NFT's URI can be modified.
*/
tfMutable = 0x00000010,
}
/**
@@ -51,6 +55,7 @@ export interface NFTokenMintFlagsInterface extends GlobalFlags {
tfOnlyXRP?: boolean
tfTrustLine?: boolean
tfTransferable?: boolean
tfMutable?: boolean
}
/**

View File

@@ -0,0 +1,67 @@
import { ValidationError } from '../../errors'
import { isHex } from '../utils'
import {
BaseTransaction,
validateBaseTransaction,
isAccount,
isString,
validateOptionalField,
Account,
validateRequiredField,
} from './common'
/**
* The NFTokenModify transaction modifies an NFToken's URI
* if its tfMutable is set to true.
*/
export interface NFTokenModify extends BaseTransaction {
TransactionType: 'NFTokenModify'
/**
* Identifies the NFTokenID of the NFToken object that the
* offer references.
*/
NFTokenID: string
/**
* Indicates the AccountID of the account that owns the corresponding NFToken.
* Can be omitted if the owner is the account submitting this transaction
*/
Owner?: Account
/**
* URI that points to the data and/or metadata associated with the NFT.
* This field need not be an HTTP or HTTPS URL; it could be an IPFS URI, a
* magnet link, immediate data encoded as an RFC2379 "data" URL, or even an
* opaque issuer-specific encoding. The URI is NOT checked for validity, but
* the field is limited to a maximum length of 256 bytes.
*
* This field must be hex-encoded. You can use `convertStringToHex` to
* convert this field to the proper encoding.
*
* This field must not be an empty string. Omit it from the transaction or
* set to `null` if you do not use it.
*/
URI?: string | null
}
/**
* Verify the form and type of an NFTokenModify at runtime.
*
* @param tx - An NFTokenModify Transaction.
* @throws When the NFTokenModify is Malformed.
*/
export function validateNFTokenModify(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'NFTokenID', isString)
validateOptionalField(tx, 'Owner', isAccount)
validateOptionalField(tx, 'URI', isString)
if (tx.URI !== undefined && typeof tx.URI === 'string') {
if (tx.URI === '') {
throw new ValidationError('NFTokenModify: URI must not be empty string')
}
if (!isHex(tx.URI)) {
throw new ValidationError('NFTokenModify: URI must be in hex format')
}
}
}

View File

@@ -7,6 +7,7 @@ import {
validateCredentialsList,
validateOptionalField,
validateRequiredField,
MAX_AUTHORIZED_CREDENTIALS,
} from './common'
/**
@@ -54,5 +55,6 @@ export function validateAccountDelete(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
MAX_AUTHORIZED_CREDENTIALS,
)
}

View File

@@ -9,15 +9,15 @@ import {
AuthorizeCredential,
Currency,
IssuedCurrencyAmount,
MPTAmount,
Memo,
Signer,
XChainBridge,
MPTAmount,
} from '../common'
import { onlyHasFields } from '../utils'
const MEMO_SIZE = 3
const MAX_CREDENTIALS_LIST_LENGTH = 8
export const MAX_AUTHORIZED_CREDENTIALS = 8
const MAX_CREDENTIAL_BYTE_LENGTH = 64
const MAX_CREDENTIAL_TYPE_LENGTH = MAX_CREDENTIAL_BYTE_LENGTH * 2
@@ -134,7 +134,9 @@ export function isIssuedCurrency(
* @param input - The input to check the form and type of
* @returns Whether the AuthorizeCredential is properly formed
*/
function isAuthorizeCredential(input: unknown): input is AuthorizeCredential {
export function isAuthorizeCredential(
input: unknown,
): input is AuthorizeCredential {
return (
isRecord(input) &&
isRecord(input.Credential) &&
@@ -455,13 +457,16 @@ export function validateCredentialType(tx: Record<string, unknown>): void {
* @param credentials An array of credential IDs to check for errors
* @param transactionType The transaction type to include in error messages
* @param isStringID Toggle for if array contains IDs instead of AuthorizeCredential objects
* @param maxCredentials The maximum length of the credentials array.
* PermissionedDomainSet transaction uses 10, other transactions use 8.
* @throws Validation Error if the formatting is incorrect
*/
// eslint-disable-next-line max-lines-per-function -- separating logic further will add unnecessary complexity
// eslint-disable-next-line max-lines-per-function, max-params -- separating logic further will add unnecessary complexity
export function validateCredentialsList(
credentials: unknown,
transactionType: string,
isStringID: boolean,
maxCredentials: number,
): void {
if (credentials == null) {
return
@@ -471,9 +476,9 @@ export function validateCredentialsList(
`${transactionType}: Credentials must be an array`,
)
}
if (credentials.length > MAX_CREDENTIALS_LIST_LENGTH) {
if (credentials.length > maxCredentials) {
throw new ValidationError(
`${transactionType}: Credentials length cannot exceed ${MAX_CREDENTIALS_LIST_LENGTH} elements`,
`${transactionType}: Credentials length cannot exceed ${maxCredentials} elements`,
)
} else if (credentials.length === 0) {
throw new ValidationError(
@@ -500,7 +505,42 @@ export function validateCredentialsList(
}
}
function containsDuplicates(objectList: object[]): boolean {
const objSet = new Set(objectList.map((obj) => JSON.stringify(obj)))
return objSet.size !== objectList.length
// Type guard to ensure we're working with AuthorizeCredential[]
// Note: This is not a rigorous type-guard. A more thorough solution would be to iterate over the array and check each item.
function isAuthorizeCredentialArray(
list: AuthorizeCredential[] | string[],
): list is AuthorizeCredential[] {
return typeof list[0] !== 'string'
}
/**
* Check if an array of objects contains any duplicates.
*
* @param objectList - Array of objects to check for duplicates
* @returns True if duplicates exist, false otherwise
*/
export function containsDuplicates(
objectList: AuthorizeCredential[] | string[],
): boolean {
// Case-1: Process a list of string-IDs
if (typeof objectList[0] === 'string') {
const objSet = new Set(objectList.map((obj) => JSON.stringify(obj)))
return objSet.size !== objectList.length
}
// Case-2: Process a list of nested objects
const seen = new Set<string>()
if (isAuthorizeCredentialArray(objectList)) {
for (const item of objectList) {
const key = `${item.Credential.Issuer}-${item.Credential.CredentialType}`
// eslint-disable-next-line max-depth -- necessary to check for type-guards
if (seen.has(key)) {
return true
}
seen.add(key)
}
}
return false
}

View File

@@ -5,6 +5,7 @@ import {
BaseTransaction,
validateBaseTransaction,
validateCredentialsList,
MAX_AUTHORIZED_CREDENTIALS,
} from './common'
/**
@@ -72,6 +73,7 @@ export function validateDepositPreauth(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- confirmed in base transaction check
tx.TransactionType as string,
false,
MAX_AUTHORIZED_CREDENTIALS,
)
} else if (tx.UnauthorizeCredentials !== undefined) {
validateCredentialsList(
@@ -79,6 +81,7 @@ export function validateDepositPreauth(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- confirmed in base transaction check
tx.TransactionType as string,
false,
MAX_AUTHORIZED_CREDENTIALS,
)
}
}

View File

@@ -7,6 +7,7 @@ import {
validateBaseTransaction,
validateCredentialsList,
validateRequiredField,
MAX_AUTHORIZED_CREDENTIALS,
} from './common'
/**
@@ -55,6 +56,7 @@ export function validateEscrowFinish(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
MAX_AUTHORIZED_CREDENTIALS,
)
if (tx.OfferSequence == null) {

View File

@@ -15,13 +15,18 @@ export {
} from './accountSet'
export { AccountDelete } from './accountDelete'
export { AMMBid } from './AMMBid'
export {
AMMClawbackFlags,
AMMClawbackFlagsInterface,
AMMClawback,
} from './AMMClawback'
export { AMMCreate } from './AMMCreate'
export { AMMDelete } from './AMMDelete'
export {
AMMDepositFlags,
AMMDepositFlagsInterface,
AMMDeposit,
} from './AMMDeposit'
export { AMMCreate } from './AMMCreate'
export { AMMVote } from './AMMVote'
export {
AMMWithdrawFlags,
@@ -71,6 +76,7 @@ export {
NFTokenMintFlags,
NFTokenMintFlagsInterface,
} from './NFTokenMint'
export { NFTokenModify, validateNFTokenModify } from './NFTokenModify'
export { OfferCancel } from './offerCancel'
export {
OfferCreateFlags,
@@ -105,3 +111,6 @@ export {
XChainModifyBridgeFlags,
XChainModifyBridgeFlagsInterface,
} from './XChainModifyBridge'
export { PermissionedDomainSet } from './permissionedDomainSet'
export { PermissionedDomainDelete } from './permissionedDomainDelete'

View File

@@ -13,6 +13,7 @@ import {
isNumber,
Account,
validateCredentialsList,
MAX_AUTHORIZED_CREDENTIALS,
} from './common'
import type { TransactionMetadataBase } from './metadata'
@@ -188,6 +189,7 @@ export function validatePayment(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
MAX_AUTHORIZED_CREDENTIALS,
)
if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') {

View File

@@ -5,6 +5,7 @@ import {
GlobalFlags,
validateBaseTransaction,
validateCredentialsList,
MAX_AUTHORIZED_CREDENTIALS,
} from './common'
/**
@@ -153,6 +154,7 @@ export function validatePaymentChannelClaim(tx: Record<string, unknown>): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
MAX_AUTHORIZED_CREDENTIALS,
)
if (tx.Channel === undefined) {

View File

@@ -0,0 +1,28 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
} from './common'
export interface PermissionedDomainDelete extends BaseTransaction {
/* The transaction type (PermissionedDomainDelete). */
TransactionType: 'PermissionedDomainDelete'
/* The domain to delete. */
DomainID: string
}
/**
* Verify the form and type of a PermissionedDomainDelete transaction.
*
* @param tx - The transaction to verify.
* @throws When the transaction is malformed.
*/
export function validatePermissionedDomainDelete(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'DomainID', isString)
}

View File

@@ -0,0 +1,54 @@
import { AuthorizeCredential } from '../common'
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateOptionalField,
validateRequiredField,
validateCredentialsList,
} from './common'
const MAX_ACCEPTED_CREDENTIALS = 10
export interface PermissionedDomainSet extends BaseTransaction {
/* The transaction type (PermissionedDomainSet). */
TransactionType: 'PermissionedDomainSet'
/* The domain to modify. Must be included if modifying an existing domain. */
DomainID?: string
/* The credentials that are accepted by the domain. Ownership of one
of these credentials automatically makes you a member of the domain.
An empty array means deleting the field. */
AcceptedCredentials: AuthorizeCredential[]
}
/**
* Validate a PermissionedDomainSet transaction.
*
* @param tx - The transaction to validate.
* @throws {ValidationError} When the transaction is invalid.
*/
export function validatePermissionedDomainSet(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'DomainID', isString)
validateRequiredField(
tx,
'AcceptedCredentials',
() => tx.AcceptedCredentials instanceof Array,
)
validateCredentialsList(
tx.AcceptedCredentials,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
// PermissionedDomainSet uses AuthorizeCredential nested objects only, strings are not allowed
false,
// PermissionedDomainSet uses at most 10 accepted credentials. This is different from Credential-feature transactions.
MAX_ACCEPTED_CREDENTIALS,
)
}

View File

@@ -4,11 +4,12 @@
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount, Memo } from '../common'
import { isHex } from '../utils'
import { setTransactionFlagsToNumber } from '../utils/flags'
import { convertTxFlagsToNumber } from '../utils/flags'
import { AccountDelete, validateAccountDelete } from './accountDelete'
import { AccountSet, validateAccountSet } from './accountSet'
import { AMMBid, validateAMMBid } from './AMMBid'
import { AMMClawback, validateAMMClawback } from './AMMClawback'
import { AMMCreate, validateAMMCreate } from './AMMCreate'
import { AMMDelete, validateAMMDelete } from './AMMDelete'
import { AMMDeposit, validateAMMDeposit } from './AMMDeposit'
@@ -57,6 +58,7 @@ import {
validateNFTokenCreateOffer,
} from './NFTokenCreateOffer'
import { NFTokenMint, validateNFTokenMint } from './NFTokenMint'
import { NFTokenModify, validateNFTokenModify } from './NFTokenModify'
import { OfferCancel, validateOfferCancel } from './offerCancel'
import { OfferCreate, validateOfferCreate } from './offerCreate'
import { OracleDelete, validateOracleDelete } from './oracleDelete'
@@ -74,6 +76,14 @@ import {
PaymentChannelFund,
validatePaymentChannelFund,
} from './paymentChannelFund'
import {
PermissionedDomainDelete,
validatePermissionedDomainDelete,
} from './permissionedDomainDelete'
import {
PermissionedDomainSet,
validatePermissionedDomainSet,
} from './permissionedDomainSet'
import { SetFee } from './setFee'
import { SetRegularKey, validateSetRegularKey } from './setRegularKey'
import { SignerListSet, validateSignerListSet } from './signerListSet'
@@ -114,6 +124,7 @@ import {
*/
export type SubmittableTransaction =
| AMMBid
| AMMClawback
| AMMCreate
| AMMDelete
| AMMDeposit
@@ -143,6 +154,7 @@ export type SubmittableTransaction =
| NFTokenCancelOffer
| NFTokenCreateOffer
| NFTokenMint
| NFTokenModify
| OfferCancel
| OfferCreate
| OracleDelete
@@ -151,6 +163,8 @@ export type SubmittableTransaction =
| PaymentChannelClaim
| PaymentChannelCreate
| PaymentChannelFund
| PermissionedDomainSet
| PermissionedDomainDelete
| SetRegularKey
| SignerListSet
| TicketCreate
@@ -255,12 +269,16 @@ export function validate(transaction: Record<string, unknown>): void {
})
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- okay here
setTransactionFlagsToNumber(tx as unknown as Transaction)
tx.Flags = convertTxFlagsToNumber(tx as unknown as Transaction)
switch (tx.TransactionType) {
case 'AMMBid':
validateAMMBid(tx)
break
case 'AMMClawback':
validateAMMClawback(tx)
break
case 'AMMCreate':
validateAMMCreate(tx)
break
@@ -377,6 +395,10 @@ export function validate(transaction: Record<string, unknown>): void {
validateNFTokenMint(tx)
break
case 'NFTokenModify':
validateNFTokenModify(tx)
break
case 'OfferCancel':
validateOfferCancel(tx)
break
@@ -409,6 +431,14 @@ export function validate(transaction: Record<string, unknown>): void {
validatePaymentChannelFund(tx)
break
case 'PermissionedDomainSet':
validatePermissionedDomainSet(tx)
break
case 'PermissionedDomainDelete':
validatePermissionedDomainDelete(tx)
break
case 'SetRegularKey':
validateSetRegularKey(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

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign -- param reassign is safe */
/* eslint-disable no-bitwise -- flags require bitwise operations */
import { ValidationError } from '../../errors'
import {
@@ -6,9 +5,9 @@ import {
AccountRootFlags,
} from '../ledger/AccountRoot'
import { AccountSetTfFlags } from '../transactions/accountSet'
import { AMMClawbackFlags } from '../transactions/AMMClawback'
import { AMMDepositFlags } from '../transactions/AMMDeposit'
import { AMMWithdrawFlags } from '../transactions/AMMWithdraw'
import { GlobalFlags } from '../transactions/common'
import { MPTokenAuthorizeFlags } from '../transactions/MPTokenAuthorize'
import { MPTokenIssuanceCreateFlags } from '../transactions/MPTokenIssuanceCreate'
import { MPTokenIssuanceSetFlags } from '../transactions/MPTokenIssuanceSet'
@@ -49,6 +48,7 @@ export function parseAccountRootFlags(
const txToFlag = {
AccountSet: AccountSetTfFlags,
AMMClawback: AMMClawbackFlags,
AMMDeposit: AMMDepositFlags,
AMMWithdraw: AMMWithdrawFlags,
MPTokenAuthorize: MPTokenAuthorizeFlags,
@@ -63,37 +63,61 @@ const txToFlag = {
XChainModifyBridge: XChainModifyBridgeFlags,
}
function isTxToFlagKey(
transactionType: string,
): transactionType is keyof typeof txToFlag {
return transactionType in txToFlag
}
/**
* Sets a transaction's flags to its numeric representation.
*
* @deprecated
* This utility function is deprecated.
* Use convertTxFlagsToNumber() instead and use the returned value to modify the Transaction.Flags from the caller.
*
* @param tx - A transaction to set its flags to its numeric representation.
*/
export function setTransactionFlagsToNumber(tx: Transaction): void {
if (tx.Flags == null) {
tx.Flags = 0
return
}
if (typeof tx.Flags === 'number') {
return
}
// eslint-disable-next-line no-console -- intended deprecation warning
console.warn(
'This function is deprecated. Use convertTxFlagsToNumber() instead and use the returned value to modify the Transaction.Flags from the caller.',
)
tx.Flags = txToFlag[tx.TransactionType]
? convertFlagsToNumber(tx.Flags, txToFlag[tx.TransactionType])
: 0
if (tx.Flags) {
// eslint-disable-next-line no-param-reassign -- intended param reassign in setter, retain old functionality for compatibility
tx.Flags = convertTxFlagsToNumber(tx)
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- added ValidationError check for flagEnum
function convertFlagsToNumber(flags: GlobalFlags, flagEnum: any): number {
return Object.keys(flags).reduce((resultFlags, flag) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access
if (flagEnum[flag] == null) {
throw new ValidationError(
`flag ${flag} doesn't exist in flagEnum: ${JSON.stringify(flagEnum)}`,
)
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access
return flags[flag] ? resultFlags | flagEnum[flag] : resultFlags
}, 0)
/**
* Returns a Transaction's Flags as its numeric representation.
*
* @param tx - A Transaction to parse Flags for
* @returns A numerical representation of a Transaction's Flags
*/
export function convertTxFlagsToNumber(tx: Transaction): number {
if (!tx.Flags) {
return 0
}
if (typeof tx.Flags === 'number') {
return tx.Flags
}
if (isTxToFlagKey(tx.TransactionType)) {
const flagEnum = txToFlag[tx.TransactionType]
return Object.keys(tx.Flags).reduce((resultFlags, flag) => {
if (flagEnum[flag] == null) {
throw new ValidationError(
`Invalid flag ${flag}. Valid flags are ${JSON.stringify(flagEnum)}`,
)
}
return tx.Flags?.[flag] ? resultFlags | flagEnum[flag] : resultFlags
}, 0)
}
return 0
}
/**
@@ -103,22 +127,24 @@ function convertFlagsToNumber(flags: GlobalFlags, flagEnum: any): number {
* @returns A map with all flags as booleans.
*/
export function parseTransactionFlags(tx: Transaction): object {
setTransactionFlagsToNumber(tx)
if (typeof tx.Flags !== 'number' || !tx.Flags || tx.Flags === 0) {
const flags = convertTxFlagsToNumber(tx)
if (flags === 0) {
return {}
}
const flags = tx.Flags
const flagsMap = {}
const booleanFlagMap = {}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- safe member access
const flagEnum = txToFlag[tx.TransactionType]
Object.values(flagEnum).forEach((flag) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- safe member access
if (typeof flag === 'string' && isFlagEnabled(flags, flagEnum[flag])) {
flagsMap[flag] = true
}
})
if (isTxToFlagKey(tx.TransactionType)) {
const transactionTypeFlags = txToFlag[tx.TransactionType]
Object.values(transactionTypeFlags).forEach((flag) => {
if (
typeof flag === 'string' &&
isFlagEnabled(flags, transactionTypeFlags[flag])
) {
booleanFlagMap[flag] = true
}
})
}
return flagsMap
return booleanFlagMap
}

View File

@@ -1,5 +1,3 @@
import { decode, encode } from 'ripple-binary-codec'
import type {
Client,
SubmitRequest,
@@ -12,6 +10,7 @@ import { ValidationError, XrplError } from '../errors'
import { Signer } from '../models/common'
import { TxResponse } from '../models/methods'
import { BaseTransaction } from '../models/transactions/common'
import { decode, encode } from '../utils'
/** Approximate time for a ledger to close, in milliseconds */
const LEDGER_CLOSE_TIME = 1000
@@ -52,7 +51,7 @@ export async function submitRequest(
failHard = false,
): Promise<SubmitResponse> {
if (!isSigned(signedTransaction)) {
throw new ValidationError('Transaction must be signed')
throw new ValidationError('Transaction must be signed.')
}
const signedTxEncoded =

View File

@@ -125,6 +125,8 @@ describe('server_info (rippled)', function () {
'build_version',
'node_size',
'initial_sync_duration_us',
'network_id',
'git',
]
assert.deepEqual(
omit(response.result.info, removeKeys),

View File

@@ -60,6 +60,7 @@ describe('server_state', function () {
load_factor_fee_queue: 256,
load_factor_fee_reference: 256,
load_factor_server: 256,
network_id: 63456,
peer_disconnects: '0',
peer_disconnects_resources: '0',
peers: 0,
@@ -116,6 +117,8 @@ describe('server_state', function () {
'node_size',
'initial_sync_duration_us',
'ports',
'git',
'network_id',
]
assert.deepEqual(
omit(response.result.state, removeKeys),

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai'
import { AccountSet, SimulateRequest } from '../../../src'
import { SimulateBinaryRequest } from '../../../src/models/methods/simulate'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
// how long before each test case times out
const TIMEOUT = 20000
describe('simulate', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'json',
async () => {
const simulateRequest: SimulateRequest = {
command: 'simulate',
tx_json: {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
NFTokenMinter: testContext.wallet.address,
},
}
const simulateResponse = await testContext.client.request(simulateRequest)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta, 'object')
assert.typeOf(simulateResponse.result.tx_json, 'object')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
it(
'binary',
async () => {
const simulateRequest: SimulateBinaryRequest = {
command: 'simulate',
tx_json: {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
},
binary: true,
}
const simulateResponse = await testContext.client.request(simulateRequest)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta_blob, 'string')
assert.typeOf(simulateResponse.result.tx_blob, 'string')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
it(
'sugar',
async () => {
const tx: AccountSet = {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
NFTokenMinter: testContext.wallet.address,
}
const simulateResponse = await testContext.client.simulate(tx)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta, 'object')
assert.typeOf(simulateResponse.result.tx_json, 'object')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,57 @@
import { AMMClawback, AMMDeposit, AMMDepositFlags, XRP } from 'xrpl'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { createAMMPool, testTransaction } from '../utils'
describe('AMMClawback', function () {
let testContext: XrplIntegrationTestContext
beforeAll(async () => {
testContext = await setupClient(serverUrl)
})
afterAll(async () => teardownClient(testContext))
it('base', async function () {
const ammPool = await createAMMPool(testContext.client, true)
const { issuerWallet } = ammPool
const holderWallet = ammPool.lpWallet
const asset = {
currency: 'USD',
issuer: issuerWallet.classicAddress,
}
const asset2 = {
currency: 'XRP',
} as XRP
const ammDepositTx: AMMDeposit = {
TransactionType: 'AMMDeposit',
Account: holderWallet.classicAddress,
Asset: asset,
Asset2: asset2,
Amount: {
currency: 'USD',
issuer: issuerWallet.address,
value: '10',
},
Flags: AMMDepositFlags.tfSingleAsset,
}
await testTransaction(testContext.client, ammDepositTx, holderWallet)
const ammClawback: AMMClawback = {
TransactionType: 'AMMClawback',
Account: issuerWallet.address,
Holder: holderWallet.address,
Asset: asset,
Asset2: asset2,
}
await testTransaction(testContext.client, ammClawback, issuerWallet)
})
})

View File

@@ -0,0 +1,104 @@
import { assert } from 'chai'
import { NFTokenModify } from '../../../dist/npm'
import { NFTokenMintFlags } from '../../../dist/npm/src'
import {
convertStringToHex,
getNFTokenID,
NFTokenMint,
TransactionMetadata,
TxRequest,
} from '../../../src'
import { hashSignedTx } from '../../../src/utils/hashes'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('NFTokenModify', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
// Mint an NFToken with tfMutable flag and modify URI later
it(
'modify NFToken URI',
async function () {
const oldUri = convertStringToHex('https://www.google.com')
const newUri = convertStringToHex('https://www.youtube.com')
const mutableMint: NFTokenMint = {
TransactionType: 'NFTokenMint',
Account: testContext.wallet.address,
Flags: NFTokenMintFlags.tfMutable,
URI: oldUri,
NFTokenTaxon: 0,
}
const response = await testTransaction(
testContext.client,
mutableMint,
testContext.wallet,
)
assert.equal(response.type, 'response')
const mutableTx: TxRequest = {
command: 'tx',
transaction: hashSignedTx(response.result.tx_blob),
}
const mutableTxResponse = await testContext.client.request(mutableTx)
const mutableNFTokenID =
getNFTokenID(
mutableTxResponse.result.meta as TransactionMetadata<NFTokenMint>,
) ?? 'undefined'
const accountNFTs = await testContext.client.request({
command: 'account_nfts',
account: testContext.wallet.address,
})
assert.equal(
accountNFTs.result.account_nfts.find(
(nft) => nft.NFTokenID === mutableNFTokenID,
)?.URI,
oldUri,
)
const modifyTx: NFTokenModify = {
TransactionType: 'NFTokenModify',
Account: testContext.wallet.address,
NFTokenID: mutableNFTokenID,
URI: newUri,
}
const modifyResponse = await testTransaction(
testContext.client,
modifyTx,
testContext.wallet,
)
assert.equal(modifyResponse.type, 'response')
const nfts = await testContext.client.request({
command: 'account_nfts',
account: testContext.wallet.address,
})
assert.equal(
nfts.result.account_nfts.find(
(nft) => nft.NFTokenID === mutableNFTokenID,
)?.URI,
newUri,
)
},
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

@@ -0,0 +1,86 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import {
LedgerEntryRequest,
PermissionedDomainDelete,
PermissionedDomainSet,
AuthorizeCredential,
} from '../../../src'
import PermissionedDomain from '../../../src/models/ledger/PermissionedDomain'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('PermissionedDomainSet', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'Lifecycle of PermissionedDomain ledger object',
async () => {
const sampleCredential: AuthorizeCredential = {
Credential: {
CredentialType: stringToHex('Passport'),
Issuer: testContext.wallet.classicAddress,
},
}
// Step-1: Test the PermissionedDomainSet transaction
const pdSet: PermissionedDomainSet = {
TransactionType: 'PermissionedDomainSet',
Account: testContext.wallet.classicAddress,
AcceptedCredentials: [sampleCredential],
}
await testTransaction(testContext.client, pdSet, testContext.wallet)
// Step-2: Validate the ledger_entry, account_objects RPC methods
// validate the account_objects RPC
const result = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'permissioned_domain',
})
assert.equal(result.result.account_objects.length, 1)
const pd = result.result.account_objects[0] as PermissionedDomain
assert.equal(pd.Flags, 0)
expect(pd.AcceptedCredentials).toEqual([sampleCredential])
// validate the ledger_entry RPC
const ledgerEntryRequest: LedgerEntryRequest = {
command: 'ledger_entry',
// fetch the PD `index` from the previous account_objects RPC response
index: pd.index,
}
const ledgerEntryResult = await testContext.client.request(
ledgerEntryRequest,
)
assert.deepEqual(pd, ledgerEntryResult.result.node)
// Step-3: Test the PDDelete transaction
const pdDelete: PermissionedDomainDelete = {
TransactionType: 'PermissionedDomainDelete',
Account: testContext.wallet.classicAddress,
// fetch the PD `index` from the previous account_objects RPC response
DomainID: pd.index,
}
await testTransaction(testContext.client, pdDelete, testContext.wallet)
},
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

@@ -373,7 +373,10 @@ export async function getIOUBalance(
return (await client.request(request)).result.lines[0].balance
}
export async function createAMMPool(client: Client): Promise<{
export async function createAMMPool(
client: Client,
enableAMMClawback = false,
): Promise<{
issuerWallet: Wallet
lpWallet: Wallet
asset: Currency
@@ -391,6 +394,16 @@ export async function createAMMPool(client: Client): Promise<{
await testTransaction(client, accountSetTx, issuerWallet)
if (enableAMMClawback) {
const accountSetTx2: AccountSet = {
TransactionType: 'AccountSet',
Account: issuerWallet.classicAddress,
SetFlag: AccountSetAsfFlags.asfAllowTrustLineClawback,
}
await testTransaction(client, accountSetTx2, issuerWallet)
}
const trustSetTx: TrustSet = {
TransactionType: 'TrustSet',
Flags: TrustSetFlags.tfClearNoRipple,

View File

@@ -0,0 +1,176 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import {
AMMClawbackFlags,
validateAMMClawback,
} from '../../src/models/transactions/AMMClawback'
/**
* AMMClawback Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('AMMClawback', function () {
let ammClawback
beforeEach(function () {
ammClawback = {
TransactionType: 'AMMClawback',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9',
Asset: {
currency: 'USD',
issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
},
Asset2: {
currency: 'XRP',
},
Amount: {
currency: 'USD',
issuer: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
value: '1000',
},
Sequence: 1337,
} as any
})
it(`verifies valid AMMClawback`, function () {
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})
it(`verifies valid AMMClawback without Amount`, function () {
delete ammClawback.Amount
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})
it(`verifies valid AMMClawback with tfClawTwoAssets`, function () {
ammClawback.flags = AMMClawbackFlags.tfClawTwoAssets
assert.doesNotThrow(() => validateAMMClawback(ammClawback))
assert.doesNotThrow(() => validate(ammClawback))
})
it(`throws w/ missing Holder`, function () {
delete ammClawback.Holder
const errorMessage = 'AMMClawback: missing field Holder'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ invalid field Holder`, function () {
ammClawback.Holder = 1234
const errorMessage = 'AMMClawback: invalid field Holder'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ Holder and Asset.issuer must be distinct`, function () {
ammClawback.Holder = ammClawback.Asset.issuer
const errorMessage = 'AMMClawback: Holder and Asset.issuer must be distinct'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ missing Asset`, function () {
delete ammClawback.Asset
const errorMessage = 'AMMClawback: missing field Asset'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ invalid field Asset`, function () {
ammClawback.Asset = '1000'
const errorMessage = 'AMMClawback: invalid field Asset'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ Account must be the same as Asset.issuer`, function () {
ammClawback.Account = 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn'
const errorMessage = 'AMMClawback: Account must be the same as Asset.issuer'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ missing Asset2`, function () {
delete ammClawback.Asset2
const errorMessage = 'AMMClawback: missing field Asset2'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ invalid field Asset2`, function () {
ammClawback.Asset2 = '1000'
const errorMessage = 'AMMClawback: invalid field Asset2'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ invalid field Amount`, function () {
ammClawback.Amount = 1000
const errorMessage = 'AMMClawback: invalid field Amount'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ Amount.currency must match Asset.currency`, function () {
ammClawback.Amount.currency = 'ETH'
const errorMessage =
'AMMClawback: Amount.currency must match Asset.currency'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
it(`throws w/ Amount.issuer must match Amount.issuer`, function () {
ammClawback.Amount.issuer = 'rnYgaEtpqpNRt3wxE39demVpDAA817rQEY'
const errorMessage = 'AMMClawback: Amount.issuer must match Amount.issuer'
assert.throws(
() => validateAMMClawback(ammClawback),
ValidationError,
errorMessage,
)
assert.throws(() => validate(ammClawback), ValidationError, errorMessage)
})
})

View File

@@ -0,0 +1,75 @@
import { assert } from 'chai'
import { convertStringToHex, validate, ValidationError } from '../../src'
const TOKEN_ID =
'00090032B5F762798A53D543A014CAF8B297CFF8F2F937E844B17C9E00000003'
/**
* NFTokenModify Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('NFTokenModify', function () {
it(`verifies valid NFTokenModify`, function () {
const validNFTokenModify = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
NFTokenID: TOKEN_ID,
Fee: '5000000',
Sequence: 2470665,
URI: convertStringToHex('http://xrpl.org'),
} as any
assert.doesNotThrow(() => validate(validNFTokenModify))
})
it(`throws w/ missing NFTokenID`, function () {
const invalid = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenModify: missing field NFTokenID',
)
})
it(`throws w/ URI being an empty string`, function () {
const invalid = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
NFTokenID: TOKEN_ID,
Fee: '5000000',
Sequence: 2470665,
URI: '',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenModify: URI must not be empty string',
)
})
it(`throws w/ URI not in hex format`, function () {
const invalid = {
TransactionType: 'NFTokenModify',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
NFTokenID: TOKEN_ID,
Fee: '5000000',
Sequence: 2470665,
URI: '--',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'NFTokenModify: URI must be in hex format',
)
})
})

View File

@@ -0,0 +1,49 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validatePermissionedDomainDelete } from '../../src/models/transactions/permissionedDomainDelete'
/**
* PermissionedDomainDelete Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('PermissionedDomainDelete', function () {
let tx
beforeEach(function () {
tx = {
TransactionType: 'PermissionedDomainDelete',
Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8',
DomainID:
'D88930B33C2B6831660BFD006D91FF100011AD4E67CBB78B460AF0A215103737',
} as any
})
it('verifies valid PermissionedDomainDelete', function () {
assert.doesNotThrow(() => validatePermissionedDomainDelete(tx))
assert.doesNotThrow(() => validate(tx))
})
it(`throws w/ missing field DomainID`, function () {
delete tx.DomainID
const errorMessage = 'PermissionedDomainDelete: missing field DomainID'
assert.throws(
() => validatePermissionedDomainDelete(tx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ invalid DomainID`, function () {
tx.DomainID = 1234
const errorMessage = 'PermissionedDomainDelete: invalid field DomainID'
assert.throws(
() => validatePermissionedDomainDelete(tx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
})

View File

@@ -0,0 +1,92 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { AuthorizeCredential, validate, ValidationError } from '../../src'
/**
* PermissionedDomainSet Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('PermissionedDomainSet', function () {
let tx
const sampleCredential: AuthorizeCredential = {
Credential: {
CredentialType: stringToHex('Passport'),
Issuer: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8',
},
}
beforeEach(function () {
tx = {
TransactionType: 'PermissionedDomainSet',
Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8',
DomainID:
'D88930B33C2B6831660BFD006D91FF100011AD4E67CBB78B460AF0A215103737',
AcceptedCredentials: [sampleCredential],
} as any
})
it('verifies valid PermissionedDomainSet', function () {
assert.doesNotThrow(() => validate(tx))
})
it(`throws with invalid field DomainID`, function () {
// DomainID is expected to be a string
tx.DomainID = 1234
const errorMessage = 'PermissionedDomainSet: invalid field DomainID'
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it(`throws w/ missing field AcceptedCredentials`, function () {
delete tx.AcceptedCredentials
const errorMessage =
'PermissionedDomainSet: missing field AcceptedCredentials'
assert.throws(() => validate(tx), ValidationError, errorMessage)
})
it('throws when AcceptedCredentials exceeds maximum length', function () {
tx.AcceptedCredentials = Array(11).fill(sampleCredential)
assert.throws(
() => validate(tx),
ValidationError,
'PermissionedDomainSet: Credentials length cannot exceed 10 elements',
)
})
it('throws when AcceptedCredentials is empty', function () {
tx.AcceptedCredentials = []
assert.throws(
() => validate(tx),
ValidationError,
'PermissionedDomainSet: Credentials cannot be an empty array',
)
})
it('throws when AcceptedCredentials is not an array type', function () {
tx.AcceptedCredentials = 'AcceptedCredentials is not an array'
assert.throws(
() => validate(tx),
ValidationError,
'PermissionedDomainSet: invalid field AcceptedCredentials',
)
})
it('throws when AcceptedCredentials contains duplicates', function () {
tx.AcceptedCredentials = [sampleCredential, sampleCredential]
assert.throws(
() => validate(tx),
ValidationError,
'PermissionedDomainSet: Credentials cannot contain duplicate elements',
)
})
it('throws when AcceptedCredentials contains invalid format', function () {
tx.AcceptedCredentials = [{ Field1: 'Value1', Field2: 'Value2' }]
assert.throws(
() => validate(tx),
ValidationError,
'PermissionedDomainSet: Invalid Credentials format',
)
})
})

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

@@ -1,3 +1,4 @@
/* eslint-disable import/no-deprecated -- using deprecated setTransactionFlagsToNumbers to ensure no breaking changes */
/* eslint-disable no-bitwise -- flags require bitwise operations */
import { assert } from 'chai'
@@ -12,10 +13,13 @@ import {
TrustSet,
TrustSetFlags,
} from '../../src'
import { AuthorizeCredential } from '../../src/models/common'
import { AccountRootFlags } from '../../src/models/ledger'
import { containsDuplicates } from '../../src/models/transactions/common'
import { isFlagEnabled } from '../../src/models/utils'
import {
setTransactionFlagsToNumber,
convertTxFlagsToNumber,
parseAccountRootFlags,
parseTransactionFlags,
} from '../../src/models/utils/flags'
@@ -26,6 +30,43 @@ import {
* Provides tests for utils used in models.
*/
describe('Models Utils', function () {
describe('validate containsDuplicates utility method', function () {
it(`use nested-objects for input parameters, list contains duplicates`, function () {
// change the order of the inner-objects in the list
const list_with_duplicates: AuthorizeCredential[] = [
{ Credential: { Issuer: 'alice', CredentialType: 'Passport' } },
{ Credential: { CredentialType: 'Passport', Issuer: 'alice' } },
]
assert.isTrue(containsDuplicates(list_with_duplicates))
})
it(`use nested-objects for input parameters, no duplicates`, function () {
const list_without_dups: AuthorizeCredential[] = [
{ Credential: { Issuer: 'alice', CredentialType: 'Passport' } },
{ Credential: { CredentialType: 'DMV_license', Issuer: 'bob' } },
]
assert.isFalse(containsDuplicates(list_without_dups))
})
it(`use string-IDs for input parameters`, function () {
const list_without_dups: string[] = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
'F9F89FBB1426210D58D6A06E5EEF1783D6A90EE403B79AEDF0FED36A6DE238D2',
'5328F2D1D6EBBC6093DC10F1EA3DD630666F5B2491EB9BDD7DF9A6C45AC12C46',
]
const list_with_duplicates: string[] = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
]
assert.isFalse(containsDuplicates(list_without_dups))
assert.isTrue(containsDuplicates(list_with_duplicates))
})
})
describe('isFlagEnabled', function () {
let flags: number
const flag1 = 0x00010000
@@ -46,6 +87,65 @@ describe('Models Utils', function () {
})
})
describe('parseAccountRootFlags', function () {
// eslint-disable-next-line complexity -- Simpler to list them all out at once.
it('all flags enabled', function () {
const accountRootFlags =
AccountRootFlags.lsfDefaultRipple |
AccountRootFlags.lsfDepositAuth |
AccountRootFlags.lsfDisableMaster |
AccountRootFlags.lsfDisallowXRP |
AccountRootFlags.lsfGlobalFreeze |
AccountRootFlags.lsfNoFreeze |
AccountRootFlags.lsfPasswordSpent |
AccountRootFlags.lsfRequireAuth |
AccountRootFlags.lsfRequireDestTag |
AccountRootFlags.lsfDisallowIncomingNFTokenOffer |
AccountRootFlags.lsfDisallowIncomingCheck |
AccountRootFlags.lsfDisallowIncomingPayChan |
AccountRootFlags.lsfDisallowIncomingTrustline |
AccountRootFlags.lsfAllowTrustLineClawback
const parsed = parseAccountRootFlags(accountRootFlags)
assert.isTrue(
parsed.lsfDefaultRipple &&
parsed.lsfDepositAuth &&
parsed.lsfDisableMaster &&
parsed.lsfDisallowXRP &&
parsed.lsfGlobalFreeze &&
parsed.lsfNoFreeze &&
parsed.lsfPasswordSpent &&
parsed.lsfRequireAuth &&
parsed.lsfRequireDestTag &&
parsed.lsfDisallowIncomingNFTokenOffer &&
parsed.lsfDisallowIncomingCheck &&
parsed.lsfDisallowIncomingPayChan &&
parsed.lsfDisallowIncomingTrustline &&
parsed.lsfAllowTrustLineClawback,
)
})
it('no flags set', function () {
const parsed = parseAccountRootFlags(0)
assert.isUndefined(parsed.lsfDefaultRipple)
assert.isUndefined(parsed.lsfDepositAuth)
assert.isUndefined(parsed.lsfDisableMaster)
assert.isUndefined(parsed.lsfDisallowXRP)
assert.isUndefined(parsed.lsfGlobalFreeze)
assert.isUndefined(parsed.lsfNoFreeze)
assert.isUndefined(parsed.lsfPasswordSpent)
assert.isUndefined(parsed.lsfRequireAuth)
assert.isUndefined(parsed.lsfRequireDestTag)
assert.isUndefined(parsed.lsfDisallowIncomingNFTokenOffer)
assert.isUndefined(parsed.lsfDisallowIncomingCheck)
assert.isUndefined(parsed.lsfDisallowIncomingPayChan)
assert.isUndefined(parsed.lsfDisallowIncomingTrustline)
assert.isUndefined(parsed.lsfAllowTrustLineClawback)
})
})
describe('setTransactionFlagsToNumber', function () {
it('sets OfferCreateFlags to its numeric value', function () {
const tx: OfferCreate = {
@@ -151,64 +251,9 @@ describe('Models Utils', function () {
setTransactionFlagsToNumber(tx)
assert.strictEqual(tx.Flags, 0)
})
})
// eslint-disable-next-line complexity -- Simpler to list them all out at once.
it('parseAccountRootFlags all enabled', function () {
const accountRootFlags =
AccountRootFlags.lsfDefaultRipple |
AccountRootFlags.lsfDepositAuth |
AccountRootFlags.lsfDisableMaster |
AccountRootFlags.lsfDisallowXRP |
AccountRootFlags.lsfGlobalFreeze |
AccountRootFlags.lsfNoFreeze |
AccountRootFlags.lsfPasswordSpent |
AccountRootFlags.lsfRequireAuth |
AccountRootFlags.lsfRequireDestTag |
AccountRootFlags.lsfDisallowIncomingNFTokenOffer |
AccountRootFlags.lsfDisallowIncomingCheck |
AccountRootFlags.lsfDisallowIncomingPayChan |
AccountRootFlags.lsfDisallowIncomingTrustline |
AccountRootFlags.lsfAllowTrustLineClawback
const parsed = parseAccountRootFlags(accountRootFlags)
assert.isTrue(
parsed.lsfDefaultRipple &&
parsed.lsfDepositAuth &&
parsed.lsfDisableMaster &&
parsed.lsfDisallowXRP &&
parsed.lsfGlobalFreeze &&
parsed.lsfNoFreeze &&
parsed.lsfPasswordSpent &&
parsed.lsfRequireAuth &&
parsed.lsfRequireDestTag &&
parsed.lsfDisallowIncomingNFTokenOffer &&
parsed.lsfDisallowIncomingCheck &&
parsed.lsfDisallowIncomingPayChan &&
parsed.lsfDisallowIncomingTrustline &&
parsed.lsfAllowTrustLineClawback,
)
})
it('parseAccountFlags all false', function () {
const parsed = parseAccountRootFlags(0)
assert.isUndefined(parsed.lsfDefaultRipple)
assert.isUndefined(parsed.lsfDepositAuth)
assert.isUndefined(parsed.lsfDisableMaster)
assert.isUndefined(parsed.lsfDisallowXRP)
assert.isUndefined(parsed.lsfGlobalFreeze)
assert.isUndefined(parsed.lsfNoFreeze)
assert.isUndefined(parsed.lsfPasswordSpent)
assert.isUndefined(parsed.lsfRequireAuth)
assert.isUndefined(parsed.lsfRequireDestTag)
assert.isUndefined(parsed.lsfDisallowIncomingNFTokenOffer)
assert.isUndefined(parsed.lsfDisallowIncomingCheck)
assert.isUndefined(parsed.lsfDisallowIncomingPayChan)
assert.isUndefined(parsed.lsfDisallowIncomingTrustline)
assert.isUndefined(parsed.lsfAllowTrustLineClawback)
})
describe('parseTransactionFlags', function () {
it('parseTransactionFlags all enabled', function () {
const tx: PaymentChannelClaim = {
Account: 'r...',
@@ -264,4 +309,111 @@ describe('Models Utils', function () {
assert.notStrictEqual(flagsMap, expected)
})
})
describe('convertTxFlagsToNumber', function () {
it('converts OfferCreateFlags to its numeric value', function () {
const tx: OfferCreate = {
Account: 'r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W',
Fee: '10',
TakerGets: {
currency: 'DSH',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '43.11584856965009',
},
TakerPays: '12928290425',
TransactionType: 'OfferCreate',
TxnSignature:
'3045022100D874CDDD6BB24ED66E83B1D3574D3ECAC753A78F26DB7EBA89EAB8E7D72B95F802207C8CCD6CEA64E4AE2014E59EE9654E02CA8F03FE7FCE0539E958EAE182234D91',
Flags: {
tfPassive: true,
tfImmediateOrCancel: false,
tfFillOrKill: true,
tfSell: false,
},
}
const { tfPassive, tfFillOrKill } = OfferCreateFlags
const expected: number = tfPassive | tfFillOrKill
const result = convertTxFlagsToNumber(tx)
assert.strictEqual(result, expected)
})
it('converts PaymentChannelClaimFlags to its numeric value', function () {
const tx: PaymentChannelClaim = {
Account: 'r...',
TransactionType: 'PaymentChannelClaim',
Channel:
'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198',
Flags: {
tfRenew: true,
tfClose: false,
},
}
const { tfRenew } = PaymentChannelClaimFlags
const expected: number = tfRenew
const result = convertTxFlagsToNumber(tx)
assert.strictEqual(result, expected)
})
it('converts PaymentTransactionFlags to its numeric value', function () {
const tx: Payment = {
TransactionType: 'Payment',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
Amount: '1234',
Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
Flags: {
tfNoRippleDirect: false,
tfPartialPayment: true,
tfLimitQuality: true,
},
}
const { tfPartialPayment, tfLimitQuality } = PaymentFlags
const expected: number = tfPartialPayment | tfLimitQuality
const result = convertTxFlagsToNumber(tx)
assert.strictEqual(result, expected)
})
it('converts TrustSetFlags to its numeric value', function () {
const tx: TrustSet = {
TransactionType: 'TrustSet',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
LimitAmount: {
currency: 'XRP',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '4329.23',
},
QualityIn: 1234,
QualityOut: 4321,
Flags: {
tfSetfAuth: true,
tfSetNoRipple: false,
tfClearNoRipple: true,
tfSetFreeze: false,
tfClearFreeze: true,
},
}
const { tfSetfAuth, tfClearNoRipple, tfClearFreeze } = TrustSetFlags
const expected: number = tfSetfAuth | tfClearNoRipple | tfClearFreeze
const result = convertTxFlagsToNumber(tx)
assert.strictEqual(result, expected)
})
it('converts other transaction types flags to its numeric value', function () {
const tx: DepositPreauth = {
TransactionType: 'DepositPreauth',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
Flags: {},
}
const result = convertTxFlagsToNumber(tx)
assert.strictEqual(result, 0)
})
})
})