Compare commits

...

43 Commits

Author SHA1 Message Date
Omar Khan
11e724253b 4.1.0 release (#2851)
* update HISTORY

* update package-lock

* update ripple-binary-codec HISTORY

* update release dates

* update release dates

* update HISTORY files
2024-12-21 07:10:25 +05:30
achowdhry-ripple
f34d1a7a63 Credentials (#2829)
* create credentials obj, modify depositpreauth

* structrure of transaction models

* initial validation methods and modify transactions affected by deposit auth

* cleanup and add new transactions to list

* binarycodec and add amendments to config

* methods account for credentials

* binary codec update

* add amendments to config

* error validation for credentials actions

* core logic of error validation completed

* type checking in error validation

* init test files and field type validations

* basic tests for crud transactions

* cred delete tests

* cred accept unit tests

* cred create and accept unit tests

* cred delete unit tests

* depositPreauth unit tests

* generic checks for payment, paymentchannelclaim, escrowfinish credential list

* ledger entry update

* lint errors

* cleanup and use helper methods

* fix lint bug

* init integration tests for new transactions

* fix build error, integration test docker update

* unit test fixes -- all pass now

* integration test layout complete

* integration command

* integration tests run

* cicd command edit

* lint and cleanup

* modified history markdown

* deposit preauth integration update

* update docs with new docker command

* fix validation for string id credential arrays

* exports

* add flag

* lint

* fix typo in contributing doc

* docstring typos

* readable string

* fix test'

* review comment fixes

* txn duplicate fix

* Apply suggestions from code review

Co-authored-by: Omar Khan <khancodegt@gmail.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>

* Apply suggestions from code review

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

* Apply suggestions from code review

Co-authored-by: Omar Khan <khancodegt@gmail.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>

* typo in auto suggest

* rebase

* readd definitions after rebase

* cleanup list val

* unit tests fixed and running

* lint

* refactor authcred check to work

* Update packages/xrpl/src/models/transactions/payment.ts

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

* typo

* Update .ci-config/rippled.cfg

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

* update rippled version

* optional field nits

* add to response depositauthorize

* Update packages/xrpl/src/models/transactions/CredentialCreate.ts

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

* Update packages/xrpl/src/models/transactions/CredentialDelete.ts

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

* Update packages/xrpl/src/models/transactions/accountDelete.ts

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

* Apply suggestions from code review

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

* cleanups

* unit test fix

* more escrowfinish tests

* clearer error message

* re add statement

* undo autodeleted mandates

* remove extraneous integration tests for now

* lint

* Update .ci-config/rippled.cfg

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

* Update packages/xrpl/src/models/transactions/common.ts

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

* added tests

* typo

---------

Co-authored-by: Omar Khan <khancodegt@gmail.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
2024-12-20 14:03:56 -05:00
Shawn Xie
7bf6fecc71 add more mpt flag validations (#2856)
* remove else condition

* validation txfee

* clidation

* lint

* lint

* comments

* lint

* more typechecking

* rm newline

* refactor

* null check

* revert null check

* reuse test
2024-12-19 15:53:16 -05:00
dependabot[bot]
303c2b983c build(deps): bump @scure/bip32 from 1.5.0 to 1.6.0 (#2852) 2024-12-16 20:26:32 +00:00
Mayukha Vadari
448164da70 chore: bump Github Actions versions (#2788)
bump Github Actions versions
2024-12-13 12:17:21 -08:00
dependabot[bot]
9f72c8d384 build(deps): bump @scure/base from 1.1.9 to 1.2.1 (#2844) 2024-12-12 01:12:16 +00:00
Mayukha Vadari
e42d418662 feat: add node v22 and npm v10 support (#2695)
* add node 22 support

* add debug stuff

* try upgrading npm

* remove node 16

* debug

* remove tmux

* only allow 22.9 onwards

* try 22.8

* try 22.7

* test more 22.x versions

* test more versions

* switch back to 22.x, clean up

* update README
2024-12-11 16:50:41 -08:00
dependabot[bot]
305f2c48bf build(deps): bump @noble/curves from 1.6.0 to 1.7.0 (#2836)
Bumps [@noble/curves](https://github.com/paulmillr/noble-curves) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/paulmillr/noble-curves/releases)
- [Commits](https://github.com/paulmillr/noble-curves/compare/1.6.0...1.7.0)

---
updated-dependencies:
- dependency-name: "@noble/curves"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 16:00:54 -08:00
dependabot[bot]
b7dfcbf075 build(deps-dev): bump react from 18.3.1 to 19.0.0 (#2843) 2024-12-11 23:37:09 +00:00
Shawn Xie
b04efe8c9e MPT (#2661)
MPT Support for library and binary codec
2024-12-11 13:38:13 -08:00
dependabot[bot]
e3188b83ed build(deps): bump @noble/hashes from 1.5.0 to 1.6.1 (#2837)
Bumps [@noble/hashes](https://github.com/paulmillr/noble-hashes) from 1.5.0 to 1.6.1.
- [Release notes](https://github.com/paulmillr/noble-hashes/releases)
- [Commits](https://github.com/paulmillr/noble-hashes/compare/1.5.0...1.6.1)

---
updated-dependencies:
- dependency-name: "@noble/hashes"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 20:35:54 +05:30
dependabot[bot]
f4011b58e7 build(deps): bump @scure/bip39 from 1.4.0 to 1.5.0 (#2838)
Bumps [@scure/bip39](https://github.com/paulmillr/scure-bip39) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/paulmillr/scure-bip39/releases)
- [Commits](https://github.com/paulmillr/scure-bip39/compare/1.4.0...1.5.0)

---
updated-dependencies:
- dependency-name: "@scure/bip39"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
2024-12-09 16:03:25 +05:30
Emmanuel Ferdman
00614753ff docs: update references in CONTRIBUTING.md (#2832)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-12-04 18:41:50 -05:00
Denis Angell
b3a76bd9c0 Update HISTORY.md (#2777) 2024-12-02 15:17:02 -05:00
dependabot[bot]
24e9ad7c12 build(deps-dev): bump webpack from 5.95.0 to 5.96.1 (#2833) 2024-11-21 20:54:39 +00:00
Shawn Xie
c2dd2edbcc Update custom Payment to a higher number in binary codec test (#2824)
* update payment custom def

* comment
2024-11-18 17:34:12 -05:00
dependabot[bot]
c9207337aa build(deps-dev): bump typedoc from 0.26.10 to 0.26.11 (#2822)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.26.10 to 0.26.11.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.26.10...v0.26.11)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Omar Khan <khancodegt@gmail.com>
2024-11-15 15:29:12 -05:00
dependabot[bot]
a6852dd588 build(deps-dev): bump @types/jest from 29.5.12 to 29.5.14 (#2821)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Omar Khan <khancodegt@gmail.com>
2024-11-15 15:06:26 -05:00
dependabot[bot]
3a604ce69a build(deps-dev): bump @types/lodash from 4.17.12 to 4.17.13 (#2819)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.12 to 4.17.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Omar Khan <khancodegt@gmail.com>
2024-11-15 14:59:19 -05:00
justinr1234
663b80f1d0 chore: remove code rabbit sequence diagram (#2827) 2024-11-14 12:40:46 -06:00
dependabot[bot]
92eb809397 build(deps): bump @noble/hashes from 1.4.0 to 1.5.0 (#2814)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: justinr1234 <justinr1234@gmail.com>
Co-authored-by: anissa-ripple <165064424+anissa-ripple@users.noreply.github.com>
2024-11-04 11:15:03 -06:00
dependabot[bot]
799cd65386 build(deps): bump @scure/bip39 from 1.3.0 to 1.4.0 (#2813)
Bumps [@scure/bip39](https://github.com/paulmillr/scure-bip39) from 1.3.0 to 1.4.0.
- [Release notes](https://github.com/paulmillr/scure-bip39/releases)
- [Commits](https://github.com/paulmillr/scure-bip39/compare/1.3.0...1.4.0)

---
updated-dependencies:
- dependency-name: "@scure/bip39"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: anissa-ripple <165064424+anissa-ripple@users.noreply.github.com>
Co-authored-by: justinr1234 <justinr1234@gmail.com>
2024-11-04 08:28:33 -08:00
dependabot[bot]
29d145138b build(deps-dev): bump eslint-plugin-import from 2.29.1 to 2.31.0 (#2799)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: anissa-ripple <165064424+anissa-ripple@users.noreply.github.com>
2024-11-04 10:10:51 -06:00
Mayukha Vadari
496f774154 fix: resolve TransactionStream and Ledger model issues (#2779)
* update TransactionStream model

* fix everywhere else

* add close_time_iso

* add close_time_iso to TransactionStream

* update HISTORY

* fix tests

* fix import

* Update packages/xrpl/src/client/partialPayment.ts
2024-11-04 10:55:47 -05:00
dependabot[bot]
0395c14392 build(deps-dev): bump @types/lodash from 4.17.7 to 4.17.12 (#2810) 2024-10-21 16:23:46 +00:00
dependabot[bot]
2a77e2cd91 build(deps-dev): bump typedoc from 0.26.9 to 0.26.10 (#2809) 2024-10-21 16:17:27 +00:00
dependabot[bot]
619c9ae13a build(deps-dev): bump typedoc from 0.26.7 to 0.26.9 (#2803) 2024-10-16 14:30:07 +00:00
Mayukha Vadari
8beb1292b5 fix: make transaction-generating scripts more robust (#2690)
* improve scripts

* Update settings.json

* update file locations after refactor
2024-10-11 14:40:19 -07:00
dependabot[bot]
649bf7d277 build(deps): bump @scure/bip32 from 1.4.0 to 1.5.0 (#2791) 2024-10-02 00:37:54 +00:00
dependabot[bot]
e0d368791b build(deps-dev): bump eslint from 8.57.0 to 8.57.1 (#2792) 2024-10-01 21:01:32 +00:00
Zhiyuan Wang
be9b48b071 Add include_deleted param to ledger_entry API (#2725)
* add nfts_by_issuer data type

* update HISTORY.md

* update HISTORY.md

* added to index and change field name

* change to added in history

* reformat change in history

* reformat history on bfts_by_issuer

* add include_deleted field

* update history

* fix an error in histroy

* changed comments

---------

Authored-by: Kassaking <kassaking7@gmail.com>
2024-10-01 16:54:00 -04:00
dependabot[bot]
55892c8b89 build(deps-dev): bump webpack from 5.93.0 to 5.95.0 (#2785) 2024-09-26 19:34:40 +00:00
dependabot[bot]
b1b4995047 build(deps): bump @noble/curves from 1.4.0 to 1.6.0 (#2769)
Bumps [@noble/curves](https://github.com/paulmillr/noble-curves) from 1.4.0 to 1.6.0.
- [Release notes](https://github.com/paulmillr/noble-curves/releases)
- [Commits](https://github.com/paulmillr/noble-curves/compare/1.4.0...1.6.0)

---
updated-dependencies:
- dependency-name: "@noble/curves"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 15:21:25 -04:00
dependabot[bot]
a726a5a4f9 build(deps-dev): bump typedoc from 0.26.4 to 0.26.7 (#2772)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.26.4 to 0.26.7.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.26.4...v0.26.7)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 15:08:41 -04:00
Chenna Keshava B S
8992d3914e remove bridge snippet test, the sidechain has been shut down (#2773)
* update Paths snippet test: perform prerequisite test setup within the test, remove dependence on theexternal state of testnet

* remove bridge snippet test, the sidechain has been shut down

* [FIX] Update paths snippet. Replace RipplePathFind with PathFind RPC (fixes #2385)
2024-09-26 14:07:56 -04:00
justinr1234
c2e01b3d10 feat: coderabbit.ai config (#2781) 2024-09-19 16:07:23 -05:00
Mayukha Vadari
8bfe5b42b8 chore: fix CodeQL issue (#2727)
fix CodeQL issue
2024-09-19 15:00:06 -04:00
Chenna Keshava B S
385a56a1f7 update Paths snippet test (#2765)
* update Paths snippet test: perform prerequisite test setup within the test, remove dependence on theexternal state of testnet

* fix lint errors

* remove bridge snippet test, the sidechain has been shut down

* [FIX] Update paths snippet. Replace RipplePathFind with PathFind RPC (fixes #2385)
2024-09-19 13:23:55 -05:00
achowdhry-ripple
f3960c3ccc Parse transaction flags into map of names:booleans (#2734)
* overview logic of parsetransactionflags

* parse transaction flags works

* basic tests

* eslint and docs

* linting

* lint

* fix typing

* test fix

* revert import delete

* lint

* integration fix

* lint

* imports

* added numeric test

* add history log

* history update
2024-08-07 11:44:57 -04:00
dependabot[bot]
a46e86f17e build(deps-dev): bump @types/lodash from 4.17.1 to 4.17.7 (#2745) 2024-07-24 15:49:13 +00:00
dependabot[bot]
8f5d210806 build(deps-dev): bump webpack from 5.92.1 to 5.93.0 (#2731)
Bumps [webpack](https://github.com/webpack/webpack) from 5.92.1 to 5.93.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.92.1...v5.93.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
2024-07-17 14:01:09 -07:00
dependabot[bot]
17c91cdd3a build(deps-dev): bump typedoc from 0.26.3 to 0.26.4 (#2732)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.26.3 to 0.26.4.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.26.3...v0.26.4)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
2024-07-17 13:54:08 -07:00
dependabot[bot]
74a41832ce build(deps-dev): bump ts-jest from 29.2.0 to 29.2.2 (#2733)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.2.0 to 29.2.2.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.2.0...v29.2.2)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-17 13:47:00 -07:00
87 changed files with 6497 additions and 1735 deletions

View File

@@ -178,3 +178,13 @@ PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
fixAMMv1_1
# 2.3.0 Amendments
fixAMMv1_2
Credentials
NFTokenMintOffer
MPTokensV1
fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2

30
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
reviews:
# Set the profile for reviews. Assertive profile yields more feedback, that may be considered nitpicky.
profile: "chill"
# Approve the review once CodeRabbit's comments are resolved. Note: In GitLab, all discussions must be resolved.
request_changes_workflow: false
# Generate a high level summary of the changes in the PR/MR description.
high_level_summary: false
# Generate a poem in the walkthrough comment.
poem: true
# Post review details on each review. Additionally, post a review status when a review is skipped in certain cases.
review_status: true
# Generate walkthrough in a markdown collapsible section.
collapse_walkthrough: false
# Generate sequence diagrams in the walkthrough.
sequence_diagrams: false
# Abort the in-progress review if the pull request is closed or merged.
abort_on_close: true
auto_review:
# Automatic Review | Automatic code review
enabled: true
# Review draft PRs/MRs.
drafts: false
# Ignore reviewing if the title of the pull request contains any of these keywords (case-insensitive).
ignore_title_keywords:
- build(
chat:
# Enable the bot to reply automatically without requiring the user to tag it.
auto_reply: true

View File

@@ -12,13 +12,11 @@
name: "CodeQL"
on:
push:
branches: [ main, 1.x ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '44 5 * * 6'
- cron: "44 5 * * 6"
jobs:
analyze:
@@ -28,40 +26,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.2.0-b3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
on:
push:
@@ -22,19 +22,19 @@ jobs:
node-version: [18.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup npm version 9
- name: Setup npm version 10
run: |
npm i -g npm@9 --registry=https://registry.npmjs.org
npm i -g npm@10 --registry=https://registry.npmjs.org
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -45,7 +45,6 @@ jobs:
key: ${{ runner.os }}-deps-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-${{ matrix.node-version }}-
${{ runner.os }}-deps-
- name: Install Dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
@@ -60,22 +59,22 @@ jobs:
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup npm version 9
- name: Setup npm version 10
run: |
npm i -g npm@9 --registry=https://registry.npmjs.org
npm i -g npm@10 --registry=https://registry.npmjs.org
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -86,7 +85,6 @@ jobs:
key: ${{ runner.os }}-deps-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-${{ matrix.node-version }}-
${{ runner.os }}-deps-
- name: Install Dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
@@ -101,27 +99,27 @@ jobs:
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Run docker in background
run: |
docker run --detach --rm --name rippled-service -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_info || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a"
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup npm version 9
- name: Setup npm version 10
run: |
npm i -g npm@9 --registry=https://registry.npmjs.org
npm i -g npm@10 --registry=https://registry.npmjs.org
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -132,7 +130,6 @@ jobs:
key: ${{ runner.os }}-deps-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-${{ matrix.node-version }}-
${{ runner.os }}-deps-
- name: Install Dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
@@ -156,24 +153,24 @@ jobs:
node-version: [18.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Run docker in background
run: |
docker run --detach --rm --name rippled-service -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_info || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a"
- name: Setup npm version 9
- name: Setup npm version 10
run: |
npm i -g npm@9 --registry=https://registry.npmjs.org
npm i -g npm@10 --registry=https://registry.npmjs.org
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -184,7 +181,6 @@ jobs:
key: ${{ runner.os }}-deps-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-${{ matrix.node-version }}-
${{ runner.os }}-deps-
- name: Install Dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
@@ -205,22 +201,22 @@ jobs:
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup npm version 9
- name: Setup npm version 10
run: |
npm i -g npm@9 --registry=https://registry.npmjs.org
npm i -g npm@10 --registry=https://registry.npmjs.org
- name: Cache node modules
id: cache-nodemodules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -231,7 +227,6 @@ jobs:
key: ${{ runner.os }}-deps-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-deps-${{ matrix.node-version }}-
${{ runner.os }}-deps-
- name: Install Dependencies
if: steps.cache-nodemodules.outputs.cache-hit != 'true'

View File

@@ -64,18 +64,20 @@ 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 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.0.0-b4 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
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'
npm run build
npm run test:integration
```
Breaking down the command:
* `docker run -p 6006:6006` starts a Docker container with an open port for admin WebSocket requests.
* `--interactive` allows you to interact with the container.
* `-t` starts a terminal in the container for you to send commands to.
* `--volume $PWD/.ci-config:/config/` identifies the `rippled.cfg` and `validators.txt` to import. It must be an absolute path, so we use `$PWD` instead of `./`.
`--rm` tells docker to close the container after processes are done running.
* `-it` allows you to interact with the container.
`--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
* `/opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg` starts `rippled` in standalone mode
* `--entrypoint bash rippleci/rippled:2.3.0-rc1` manually overrides the entrypoint (for versions of rippled >= 2.3.0)
* `-c 'rippled -a'` provides the bash command to start `rippled` in standalone mode from the manual entrypoint
### Browser Tests
@@ -90,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 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.2.0-b3 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
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'
npm run test:browser
```
@@ -130,13 +132,13 @@ For every file in `src`, we try to have a corresponding file in `test` with unit
The goal is to maintain above 80% code coverage, and generally any new feature or bug fix should be accompanied by unit tests, and integration tests if applicable.
For an example of a unit test, check out the [autofill tests here](./packages/xrpl/test/client/autofill.ts).
For an example of a unit test, check out the [autofill tests here](./packages/xrpl/test/client/autofill.test.ts).
If your code connects to the ledger (ex. Adding a new transaction type) it's handy to write integration tests to ensure that you can successfully interact with the ledger. Integration tests are generally run against a docker instance of rippled which contains the latest updates. Since standalone mode allows us to manually close ledgers, this allows us to run integration tests at a much faster rate than if we had to wait 4-5 seconds per transaction for the ledger to validate the transaction. [See above](#running-tests) for how to start up the docker container to run integration tests.
All integration tests should be written in the `test/integration` folder, with new `Requests` and `Transactions` tests being in their respective folders.
For an example of how to write an integration test for `xrpl.js`, you can look at the [Payment integration test](./packages/xrpl/test/integration/transactions/payment.ts).
For an example of how to write an integration test for `xrpl.js`, you can look at the [Payment integration test](./packages/xrpl/test/integration/transactions/payment.test.ts).
## Generate reference docs

View File

@@ -25,7 +25,7 @@ All of which works in Node.js (tested for v18+) & web browsers (tested for Chrom
### Requirements
+ **[Node.js v18](https://nodejs.org/)** is recommended. We also support v20. Other versions may work but are not frequently tested.
+ **[Node.js v18](https://nodejs.org/)** is recommended. We also support v20 and v22. Other versions may work but are not frequently tested.
### Installing xrpl.js

1398
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,6 @@
],
"engines": {
"node": ">=18.0.0",
"npm": ">=7.10.0 < 10.0.0"
"npm": ">=7.10.0"
}
}

View File

@@ -2,6 +2,11 @@
## Unreleased
## 2.2.0 (2024-12-23)
### Added
* Support for the Multi-Purpose Token amendment (XLS-33)
## 2.1.0 (2024-06-03)
### Added

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { JsonObject, SerializedType } from './serialized-type'
import BigNumber from 'bignumber.js'
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
import { readUInt32BE, writeUInt32BE } from '../utils'
import { Hash192 } from './hash-192'
/**
* Constants for validating amounts
@@ -16,6 +17,7 @@ const MAX_IOU_PRECISION = 16
const MAX_DROPS = new BigNumber('1e17')
const MIN_XRP = new BigNumber('1e-6')
const mask = BigInt(0x00000000ffffffff)
const mptMask = BigInt(0x8000000000000000)
/**
* BigNumber configuration for Amount IOUs
@@ -27,20 +29,28 @@ BigNumber.config({
],
})
/**
* Interface for JSON objects that represent amounts
*/
interface AmountObject extends JsonObject {
interface AmountObjectIOU extends JsonObject {
value: string
currency: string
issuer: string
}
interface AmountObjectMPT extends JsonObject {
value: string
mpt_issuance_id: string
}
/**
* Type guard for AmountObject
* Interface for JSON objects that represent amounts
*/
function isAmountObject(arg): arg is AmountObject {
type AmountObject = AmountObjectIOU | AmountObjectMPT
/**
* Type guard for AmountObjectIOU
*/
function isAmountObjectIOU(arg): arg is AmountObjectIOU {
const keys = Object.keys(arg).sort()
return (
keys.length === 3 &&
keys[0] === 'currency' &&
@@ -49,6 +59,17 @@ function isAmountObject(arg): arg is AmountObject {
)
}
/**
* Type guard for AmountObjectMPT
*/
function isAmountObjectMPT(arg): arg is AmountObjectMPT {
const keys = Object.keys(arg).sort()
return (
keys.length === 2 && keys[0] === 'mpt_issuance_id' && keys[1] === 'value'
)
}
/**
* Class for serializing/Deserializing Amounts
*/
@@ -60,7 +81,7 @@ class Amount extends SerializedType {
}
/**
* Construct an amount from an IOU or string amount
* Construct an amount from an IOU, MPT or string amount
*
* @param value An Amount, object representing an IOU, or a string
* representing an integer amount
@@ -88,7 +109,7 @@ class Amount extends SerializedType {
return new Amount(amount)
}
if (isAmountObject(value)) {
if (isAmountObjectIOU(value)) {
const number = new BigNumber(value.value)
Amount.assertIouIsValid(number)
@@ -124,6 +145,24 @@ class Amount extends SerializedType {
return new Amount(concat([amount, currency, issuer]))
}
if (isAmountObjectMPT(value)) {
Amount.assertMptIsValid(value.value)
let leadingByte = new Uint8Array(1)
leadingByte[0] |= 0x60
const num = BigInt(value.value)
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
writeUInt32BE(intBuf[0], Number(num >> BigInt(32)), 0)
writeUInt32BE(intBuf[1], Number(num & BigInt(mask)), 0)
amount = concat(intBuf)
const mptIssuanceID = Hash192.from(value.mpt_issuance_id).toBytes()
return new Amount(concat([leadingByte, amount, mptIssuanceID]))
}
throw new Error('Invalid type to construct an Amount')
}
@@ -134,8 +173,12 @@ class Amount extends SerializedType {
* @returns An Amount object
*/
static fromParser(parser: BinaryParser): Amount {
const isXRP = parser.peek() & 0x80
const numBytes = isXRP ? 48 : 8
const isIOU = parser.peek() & 0x80
if (isIOU) return new Amount(parser.read(48))
// the amount can be either MPT or XRP at this point
const isMPT = parser.peek() & 0x20
const numBytes = isMPT ? 33 : 8
return new Amount(parser.read(numBytes))
}
@@ -156,7 +199,9 @@ class Amount extends SerializedType {
const num = (msb << BigInt(32)) | lsb
return `${sign}${num.toString()}`
} else {
}
if (this.isIOU()) {
const parser = new BinaryParser(this.toString())
const mantissa = parser.read(8)
const currency = Currency.fromParser(parser) as Currency
@@ -182,6 +227,27 @@ class Amount extends SerializedType {
issuer: issuer.toJSON(),
}
}
if (this.isMPT()) {
const parser = new BinaryParser(this.toString())
const leadingByte = parser.read(1)
const amount = parser.read(8)
const mptID = Hash192.fromParser(parser) as Hash192
const isPositive = leadingByte[0] & 0x40
const sign = isPositive ? '' : '-'
const msb = BigInt(readUInt32BE(amount.slice(0, 4), 0))
const lsb = BigInt(readUInt32BE(amount.slice(4), 0))
const num = (msb << BigInt(32)) | lsb
return {
value: `${sign}${num.toString()}`,
mpt_issuance_id: mptID.toString(),
}
}
throw new Error('Invalid amount to construct JSON')
}
/**
@@ -224,6 +290,29 @@ class Amount extends SerializedType {
}
}
/**
* Validate MPT.value amount
*
* @param decimal BigNumber object representing MPT.value
* @returns void, but will throw if invalid amount
*/
private static assertMptIsValid(amount: string): void {
if (amount.indexOf('.') !== -1) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
const decimal = new BigNumber(amount)
if (!decimal.isZero()) {
if (decimal < BigNumber(0)) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
if (Number(BigInt(amount) & BigInt(mptMask)) != 0) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
}
}
/**
* Ensure that the value after being multiplied by the exponent does not
* contain a decimal.
@@ -248,7 +337,25 @@ class Amount extends SerializedType {
* @returns true if Native (XRP)
*/
private isNative(): boolean {
return (this.bytes[0] & 0x80) === 0
return (this.bytes[0] & 0x80) === 0 && (this.bytes[0] & 0x20) === 0
}
/**
* Test if this amount is in units of MPT
*
* @returns true if MPT
*/
private isMPT(): boolean {
return (this.bytes[0] & 0x80) === 0 && (this.bytes[0] & 0x20) !== 0
}
/**
* Test if this amount is in units of IOU
*
* @returns true if IOU
*/
private isIOU(): boolean {
return (this.bytes[0] & 0x80) !== 0
}
}

View File

@@ -0,0 +1,19 @@
import { Hash } from './hash'
/**
* Hash with a width of 192 bits
*/
class Hash192 extends Hash {
static readonly width = 24
static readonly ZERO_192: Hash192 = new Hash192(new Uint8Array(Hash192.width))
constructor(bytes?: Uint8Array) {
if (bytes && bytes.byteLength === 0) {
bytes = Hash192.ZERO_192.bytes
}
super(bytes ?? Hash192.ZERO_192.bytes)
}
}
export { Hash192 }

View File

@@ -4,6 +4,7 @@ import { Blob } from './blob'
import { Currency } from './currency'
import { Hash128 } from './hash-128'
import { Hash160 } from './hash-160'
import { Hash192 } from './hash-192'
import { Hash256 } from './hash-256'
import { Issue } from './issue'
import { PathSet } from './path-set'
@@ -25,6 +26,7 @@ const coreTypes: Record<string, typeof SerializedType> = {
Currency,
Hash128,
Hash160,
Hash192,
Hash256,
Issue,
PathSet,
@@ -51,6 +53,7 @@ export {
Currency,
Hash128,
Hash160,
Hash192,
Hash256,
PathSet,
STArray,

View File

@@ -67,7 +67,7 @@ class SerializedType {
* Can be customized for sidechains and amendments.
* @returns any type, if not overloaded returns hexString representation of bytes
*/
toJSON(_definitions?: XrplDefinitionsBase): JSON {
toJSON(_definitions?: XrplDefinitionsBase, _fieldName?: string): JSON {
return this.toHex()
}

View File

@@ -10,6 +10,7 @@ import { BinaryParser } from '../serdes/binary-parser'
import { BinarySerializer, BytesList } from '../serdes/binary-serializer'
import { STArray } from './st-array'
import { UInt64 } from './uint-64'
const OBJECT_END_MARKER_BYTE = Uint8Array.from([0xe1])
const OBJECT_END_MARKER = 'ObjectEndMarker'
@@ -137,6 +138,8 @@ class STObject extends SerializedType {
? this.from(xAddressDecoded[field.name], undefined, definitions)
: field.type.name === 'STArray'
? STArray.from(xAddressDecoded[field.name], definitions)
: field.type.name === 'UInt64'
? UInt64.from(xAddressDecoded[field.name], field.name)
: field.associatedType.from(xAddressDecoded[field.name])
if (associatedValue == undefined) {
@@ -182,7 +185,7 @@ class STObject extends SerializedType {
accumulator[field.name] = objectParser
.readFieldValue(field)
.toJSON(definitions)
.toJSON(definitions, field.name)
}
return accumulator

View File

@@ -2,10 +2,20 @@ import { UInt } from './uint'
import { BinaryParser } from '../serdes/binary-parser'
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
import { readUInt32BE, writeUInt32BE } from '../utils'
import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums'
const HEX_REGEX = /^[a-fA-F0-9]{1,16}$/
const BASE10_REGEX = /^[0-9]{1,20}$/
const mask = BigInt(0x00000000ffffffff)
function useBase10(fieldName: string): boolean {
return (
fieldName === 'MaximumAmount' ||
fieldName === 'OutstandingAmount' ||
fieldName === 'MPTAmount'
)
}
/**
* Derived UInt class for serializing/deserializing 64 bit UInt
*/
@@ -29,7 +39,10 @@ class UInt64 extends UInt {
* @param val A UInt64, hex-string, bigInt, or number
* @returns A UInt64 object
*/
static from<T extends UInt64 | string | bigint | number>(val: T): UInt64 {
static from<T extends UInt64 | string | bigint | number>(
val: T,
fieldName = '',
): UInt64 {
if (val instanceof UInt64) {
return val
}
@@ -51,11 +64,18 @@ class UInt64 extends UInt {
}
if (typeof val === 'string') {
if (!HEX_REGEX.test(val)) {
if (useBase10(fieldName)) {
if (!BASE10_REGEX.test(val)) {
throw new Error(`${fieldName} ${val} is not a valid base 10 string`)
}
val = BigInt(val).toString(16) as T
}
if (typeof val === 'string' && !HEX_REGEX.test(val)) {
throw new Error(`${val} is not a valid hex-string`)
}
const strBuf = val.padStart(16, '0')
const strBuf = (val as string).padStart(16, '0')
buf = hexToBytes(strBuf)
return new UInt64(buf)
}
@@ -76,8 +96,16 @@ class UInt64 extends UInt {
*
* @returns a hex-string
*/
toJSON(): string {
return bytesToHex(this.bytes)
toJSON(
_definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
fieldName = '',
): string {
const hexString = bytesToHex(this.bytes)
if (useBase10(fieldName)) {
return BigInt('0x' + hexString).toString(10)
}
return hexString
}
/**

View File

@@ -1,5 +1,7 @@
import { coreTypes } from '../src/types'
import fixtures from './fixtures/data-driven-tests.json'
import { makeParser } from '../src/binary'
const { Amount } = coreTypes
function amountErrorTests() {
@@ -25,6 +27,16 @@ describe('Amount', function () {
it('can be parsed from', function () {
expect(Amount.from('1000000') instanceof Amount).toBe(true)
expect(Amount.from('1000000').toJSON()).toEqual('1000000')
// it not valid to have negative XRP. But we test it anyways
// to ensure logic correctness for toJSON of the Amount class
{
const parser = makeParser('0000000000000001')
const value = parser.readType(Amount)
const json = value.toJSON()
expect(json).toEqual('-1')
}
const fixture = {
value: '1',
issuer: '0000000000000000000000000000000000000000',
@@ -38,5 +50,35 @@ describe('Amount', function () {
}
expect(amt.toJSON()).toEqual(rewritten)
})
it('can be parsed from MPT', function () {
let fixture = {
value: '100',
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
}
let amt = Amount.from(fixture)
expect(amt.toJSON()).toEqual(fixture)
fixture = {
value: '9223372036854775807',
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
}
amt = Amount.from(fixture)
expect(amt.toJSON()).toEqual(fixture)
// it not valid to have negative MPT. But we test it anyways
// to ensure logic correctness for toJSON of the Amount class
{
const parser = makeParser(
'20000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF',
)
const value = parser.readType(Amount)
const json = value.toJSON()
expect(json).toEqual({
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
value: '-100',
})
}
})
amountErrorTests()
})

View File

@@ -22,6 +22,7 @@ function assertEqualAmountJSON(actual, expected) {
}
expect(actual.currency).toEqual(expected.currency)
expect(actual.issuer).toEqual(expected.issuer)
expect(actual.mpt_issuance_id).toEqual(expected.mpt_issuance_id)
expect(
actual.value === expected.value ||
new BigNumber(actual.value).eq(new BigNumber(expected.value)),
@@ -207,12 +208,12 @@ function amountParsingTests() {
return
}
const parser = makeParser(f.expected_hex)
const testName = `values_tests[${i}] parses ${f.expected_hex.slice(
const hexToJsonTestName = `values_tests[${i}] parses ${f.expected_hex.slice(
0,
16,
)}...
as ${JSON.stringify(f.test_json)}`
it(testName, () => {
it(hexToJsonTestName, () => {
const value = parser.readType(Amount)
// May not actually be in canonical form. The fixtures are to be used
// also for json -> binary;
@@ -223,6 +224,15 @@ function amountParsingTests() {
expect((exponent.e ?? 0) - 15).toEqual(f?.exponent)
}
})
const jsonToHexTestName = `values_tests[${i}] parses ${JSON.stringify(
f.test_json,
)}...
as ${f.expected_hex.slice(0, 16)}`
it(jsonToHexTestName, () => {
const amt = Amount.from(f.test_json)
expect(amt.toHex()).toEqual(f.expected_hex)
})
})
}

View File

@@ -2499,7 +2499,7 @@
"type_id": 6,
"is_native": true,
"type": "Amount",
"expected_hex": "0000000000000001",
"error": "Value is negative",
"is_negative": true
},
{
@@ -2914,6 +2914,170 @@
"type": "Amount",
"error": "10000000000000000000 absolute XRP is bigger than max native value 100000000000.0",
"is_negative": true
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "9223372036854775808"
},
"type": "Amount",
"error": "Value is too large"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "18446744073709551615"
},
"type": "Amount",
"error": "Value is too large"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "-1"
},
"type": "Amount",
"error": "Value is negative"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10.1"
},
"type": "Amount",
"error": "Value has decimal point"
},
{
"test_json": {
"mpt_issuance_id": "10",
"value": "10"
},
"type": "Amount",
"error": "mpt_issuance_id has invalid hash length"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10",
"issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji"
},
"type": "Amount",
"error": "Issuer not valid for MPT"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10",
"currency": "USD"
},
"type": "Amount",
"error": "Currency not valid for MPT"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "a"
},
"type": "Amount",
"error": "Value has incorrect hex format"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xy"
},
"type": "Amount",
"error": "Value has bad hex character"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "/"
},
"type": "Amount",
"error": "Value has bad character"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0x8000000000000000"
},
"type": "Amount",
"error": "Hex value out of range"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xFFFFFFFFFFFFFFFF"
},
"type": "Amount",
"error": "Hex value out of range"
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "9223372036854775807"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "-0"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "100"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xa"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000A00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0x7FFFFFFFFFFFFFFF"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
}
]
}

View File

@@ -1,4 +1,11 @@
import { Hash128, Hash160, Hash256, AccountID, Currency } from '../src/types'
import {
Hash128,
Hash160,
Hash192,
Hash256,
AccountID,
Currency,
} from '../src/types'
describe('Hash128', function () {
it('has a static width member', function () {
@@ -51,6 +58,33 @@ describe('Hash160', function () {
})
})
describe('Hash192', function () {
it('has a static width member', function () {
expect(Hash192.width).toBe(24)
})
it('has a ZERO_192 member', function () {
expect(Hash192.ZERO_192.toJSON()).toBe(
'000000000000000000000000000000000000000000000000',
)
})
it('can be compared against another', function () {
const h1 = Hash192.from('100000000000000000000000000000000000000000000000')
const h2 = Hash192.from('200000000000000000000000000000000000000000000000')
const h3 = Hash192.from('000000000000000000000000000000000000000000000003')
expect(h1.lt(h2)).toBe(true)
expect(h3.lt(h2)).toBe(true)
})
it('throws when constructed from invalid hash length', () => {
expect(() =>
Hash192.from('10000000000000000000000000000000000000000000000'),
).toThrow(new Error('Invalid Hash length 23'))
expect(() =>
Hash192.from('10000000000000000000000000000000000000000000000000'),
).toThrow(new Error('Invalid Hash length 25'))
})
})
describe('Hash256', function () {
it('has a static width member', function () {
expect(Hash256.width).toBe(32)

View File

@@ -73,7 +73,9 @@ describe('Signing data', function () {
const customPaymentDefinitions = JSON.parse(
JSON.stringify(normalDefinitions),
)
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 31
// custom number would need to updated in case it has been used by an existing transaction type
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 200
const newDefs = new XrplDefinitions(customPaymentDefinitions)
const actual = encodeForSigning(tx_json, newDefs)
@@ -82,7 +84,7 @@ describe('Signing data', function () {
'53545800', // signingPrefix
// TransactionType
'12',
'001F',
'00C8',
// Flags
'22',
'80000000',
@@ -176,7 +178,9 @@ describe('Signing data', function () {
const customPaymentDefinitions = JSON.parse(
JSON.stringify(normalDefinitions),
)
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 31
// custom number would need to updated in case it has been used by an existing transaction type
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 200
const newDefs = new XrplDefinitions(customPaymentDefinitions)
const signingAccount = 'rJZdUusLDtY9NEsGea7ijqhVrXv98rYBYN'
@@ -187,7 +191,7 @@ describe('Signing data', function () {
'534D5400', // signingPrefix
// TransactionType
'12',
'001F',
'00C8',
// Flags
'22',
'80000000',

View File

@@ -1,5 +1,5 @@
import { UInt8, UInt64 } from '../src/types'
import { encode } from '../src'
import { encode, decode } from '../src'
const binary =
'11007222000300003700000000000000003800000000000000006280000000000000000000000000000000000000005553440000000000000000000000000000000000000000000000000166D5438D7EA4C680000000000000000000000000005553440000000000AE123A8556F3CF91154711376AFB0F894F832B3D67D5438D7EA4C680000000000000000000000000005553440000000000F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F'
@@ -96,6 +96,40 @@ const jsonEntry2 = {
index: '0000041EFD027808D3F78C8352F97E324CB816318E00B977C74ECDDC7CD975B2',
}
const mptIssuanceEntryBinary =
'11007E220000006224000002DF25000002E434000000000000000030187FFFFFFFFFFFFFFF30190000000000000064552E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE701EC1EC7B226E616D65223A2255532054726561737572792042696C6C20546F6B656E222C2273796D626F6C223A225553544254222C22646563696D616C73223A322C22746F74616C537570706C79223A313030303030302C22697373756572223A225553205472656173757279222C22697373756544617465223A22323032342D30332D3235222C226D6174757269747944617465223A22323032352D30332D3235222C226661636556616C7565223A2231303030222C22696E74657265737452617465223A22322E35222C22696E7465726573744672657175656E6379223A22517561727465726C79222C22636F6C6C61746572616C223A22555320476F7665726E6D656E74222C226A7572697364696374696F6E223A22556E6974656420537461746573222C22726567756C61746F7279436F6D706C69616E6365223A2253454320526567756C6174696F6E73222C22736563757269747954797065223A2254726561737572792042696C6C222C2265787465726E616C5F75726C223A2268747470733A2F2F6578616D706C652E636F6D2F742D62696C6C2D746F6B656E2D6D657461646174612E6A736F6E227D8414A4D893CFBC4DC6AE877EB585F90A3B47528B958D051003'
const mptIssuanceEntryJson = {
AssetScale: 3,
Flags: 98,
Issuer: 'rGpdGXDV2RFPeLEfWS9RFo5Nh9cpVDToZa',
LedgerEntryType: 'MPTokenIssuance',
MPTokenMetadata:
'7B226E616D65223A2255532054726561737572792042696C6C20546F6B656E222C2273796D626F6C223A225553544254222C22646563696D616C73223A322C22746F74616C537570706C79223A313030303030302C22697373756572223A225553205472656173757279222C22697373756544617465223A22323032342D30332D3235222C226D6174757269747944617465223A22323032352D30332D3235222C226661636556616C7565223A2231303030222C22696E74657265737452617465223A22322E35222C22696E7465726573744672657175656E6379223A22517561727465726C79222C22636F6C6C61746572616C223A22555320476F7665726E6D656E74222C226A7572697364696374696F6E223A22556E6974656420537461746573222C22726567756C61746F7279436F6D706C69616E6365223A2253454320526567756C6174696F6E73222C22736563757269747954797065223A2254726561737572792042696C6C222C2265787465726E616C5F75726C223A2268747470733A2F2F6578616D706C652E636F6D2F742D62696C6C2D746F6B656E2D6D657461646174612E6A736F6E227D',
MaximumAmount: '9223372036854775807',
OutstandingAmount: '100',
OwnerNode: '0000000000000000',
PreviousTxnID:
'2E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE',
PreviousTxnLgrSeq: 740,
Sequence: 735,
}
const mptokenEntryJson = {
Account: 'raDQsd1s8rqGjL476g59a9vVNi1rSwrC44',
Flags: 0,
LedgerEntryType: 'MPToken',
MPTAmount: '100',
MPTokenIssuanceID: '000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC',
OwnerNode: '0000000000000000',
PreviousTxnID:
'222EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C8',
PreviousTxnLgrSeq: 741,
}
const mptokenEntryBinary =
'11007F220000000025000002E5340000000000000000301A000000000000006455222EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C881143930DB9A74C26D96CB58ADFFD7E8BB78BCFE62340115000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC'
it('compareToTests[0]', () => {
expect(UInt8.from(124).compareTo(UInt64.from(124))).toBe(0)
})
@@ -144,3 +178,20 @@ it('valueOf tests', () => {
expect(val.valueOf() | 0x2).toBe(3)
})
it('UInt64 is parsed as base 10 for MPT amounts', () => {
expect(encode(mptIssuanceEntryJson)).toEqual(mptIssuanceEntryBinary)
expect(decode(mptIssuanceEntryBinary)).toEqual(mptIssuanceEntryJson)
expect(encode(mptokenEntryJson)).toEqual(mptokenEntryBinary)
expect(decode(mptokenEntryBinary)).toEqual(mptokenEntryJson)
const decodedIssuance = decode(mptIssuanceEntryBinary)
expect(typeof decodedIssuance.MaximumAmount).toBe('string')
expect(decodedIssuance.MaximumAmount).toBe('9223372036854775807')
expect(decodedIssuance.OutstandingAmount).toBe('100')
const decodedToken = decode(mptokenEntryBinary)
expect(typeof decodedToken.MPTAmount).toBe('string')
expect(decodedToken.MPTAmount).toBe('100')
})

View File

@@ -2,6 +2,21 @@
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
## 4.1.0 (2024-12-23)
### Added
* Added new MPT transaction definitions (XLS-33)
* 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)
### Fixed
* `TransactionStream` model supports APIv2
* `TransactionStream` model includes `close_time_iso` field
* `Ledger` model includes `close_time_iso` field
## 4.0.0 (2024-07-15)
### BREAKING CHANGES
@@ -9,6 +24,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### Added
* Add `nfts_by_issuer` clio-only API definition
* Add `include_deleted` to ledgerEntry request and `deleted_ledger_index` to ledgerEntry response
* Support for the `fixPreviousTxnID` amendment.
* Support for the user version of the `feature` RPC.
* Add `hash` field to `ledger` command response
@@ -36,6 +52,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## 3.0.0 (2024-02-01)
### BREAKING CHANGES
* The default signing algorithm in the `Wallet` was changed from secp256k1 to ed25519
* Bump typescript to 5.x
* Remove Node 14 support
* Remove `crypto` polyfills, `create-hash`, `elliptic`, `hash.js`, and their many dependencies in favor of `@noble/hashes` and `@nobel/curves`

View File

@@ -1,6 +1,6 @@
{
"name": "xrpl",
"version": "4.0.0",
"version": "4.1.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.1.0",
"ripple-binary-codec": "^2.2.0",
"ripple-keypairs": "^2.0.0"
},
"devDependencies": {
@@ -41,9 +41,9 @@
"karma-jasmine": "^5.1.0",
"karma-webpack": "^5.0.0",
"lodash": "^4.17.4",
"react": "^18.2.0",
"react": "^19.0.0",
"run-s": "^0.0.0",
"typedoc": "0.26.3",
"typedoc": "0.26.11",
"ws": "^8.14.2"
},
"resolutions": {

View File

@@ -1,170 +0,0 @@
/* eslint-disable max-depth -- needed for attestation checking */
/* eslint-disable @typescript-eslint/consistent-type-assertions -- needed here */
/* eslint-disable no-await-in-loop -- needed here */
import {
AccountObjectsRequest,
LedgerEntry,
Client,
XChainAccountCreateCommit,
XChainBridge,
XChainCommit,
XChainCreateClaimID,
xrpToDrops,
Wallet,
getXChainClaimID,
} from '../../src'
async function sleep(sec: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, sec * 1000)
})
}
const lockingClient = new Client('wss://s.devnet.rippletest.net:51233')
const issuingClient = new Client(
'wss://sidechain-net2.devnet.rippletest.net:51233',
)
const MAX_LEDGERS_WAITED = 5
const LEDGER_CLOSE_TIME = 4
void bridgeTransfer()
async function bridgeTransfer(): Promise<void> {
await lockingClient.connect()
await issuingClient.connect()
const lockingChainDoor = 'rnQAXXWoFNN6PEqwqsdTngCtFPCrmfuqFJ'
const accountObjectsRequest: AccountObjectsRequest = {
command: 'account_objects',
account: lockingChainDoor,
type: 'bridge',
}
const lockingAccountObjects = (
await lockingClient.request(accountObjectsRequest)
).result.account_objects
// There will only be one here - a door account can only have one bridge per currency
const bridgeData = lockingAccountObjects.filter(
(obj) =>
obj.LedgerEntryType === 'Bridge' &&
obj.XChainBridge.LockingChainIssue.currency === 'XRP',
)[0] as LedgerEntry.Bridge
const bridge: XChainBridge = bridgeData.XChainBridge
console.log(bridge)
console.log('Creating wallet on the locking chain via the faucet...')
const { wallet: wallet1 } = await lockingClient.fundWallet()
console.log(wallet1)
const wallet2 = Wallet.generate()
console.log(
`Creating ${wallet2.classicAddress} on the issuing chain via the bridge...`,
)
const fundTx: XChainAccountCreateCommit = {
TransactionType: 'XChainAccountCreateCommit',
Account: wallet1.classicAddress,
XChainBridge: bridge,
SignatureReward: bridgeData.SignatureReward,
Destination: wallet2.classicAddress,
Amount: (
parseInt(bridgeData.MinAccountCreateAmount as string, 10) * 2
).toString(),
}
const fundResponse = await lockingClient.submitAndWait(fundTx, {
wallet: wallet1,
})
console.log(fundResponse)
console.log(
'Waiting for the attestation to go through... (usually 8-12 seconds)',
)
let ledgersWaited = 0
let initialBalance = 0
while (ledgersWaited < MAX_LEDGERS_WAITED) {
await sleep(LEDGER_CLOSE_TIME)
try {
initialBalance = await issuingClient.getXrpBalance(wallet2.classicAddress)
console.log(
`Wallet ${wallet2.classicAddress} has been funded with a balance of ${initialBalance} XRP`,
)
break
} catch (_error) {
ledgersWaited += 1
if (ledgersWaited === MAX_LEDGERS_WAITED) {
// This error should never be hit if the bridge is running
throw Error('Destination account creation via the bridge failed.')
}
}
}
console.log(
`Transferring funds from ${wallet1.classicAddress} on the locking chain to ` +
`${wallet2.classicAddress} on the issuing_chain...`,
)
// Fetch the claim ID for the transfer
console.log('Step 1: Fetching the claim ID for the transfer...')
const claimIdTx: XChainCreateClaimID = {
TransactionType: 'XChainCreateClaimID',
Account: wallet2.classicAddress,
XChainBridge: bridge,
SignatureReward: bridgeData.SignatureReward,
OtherChainSource: wallet1.classicAddress,
}
const claimIdResult = await issuingClient.submitAndWait(claimIdTx, {
wallet: wallet2,
})
console.log(claimIdResult)
// Extract new claim ID from metadata
const xchainClaimId = getXChainClaimID(claimIdResult.result.meta)
if (xchainClaimId == null) {
// This shouldn't trigger assuming the transaction succeeded
throw Error('Could not extract XChainClaimID')
}
console.log(`Claim ID for the transfer: ${xchainClaimId}`)
console.log(
'Step 2: Locking the funds on the locking chain with an XChainCommit transaction...',
)
const commitTx: XChainCommit = {
TransactionType: 'XChainCommit',
Account: wallet1.classicAddress,
Amount: xrpToDrops(1),
XChainBridge: bridge,
XChainClaimID: xchainClaimId,
OtherChainDestination: wallet2.classicAddress,
}
const commitResult = await lockingClient.submitAndWait(commitTx, {
wallet: wallet1,
})
console.log(commitResult)
console.log(
'Waiting for the attestation to go through... (usually 8-12 seconds)',
)
ledgersWaited = 0
while (ledgersWaited < MAX_LEDGERS_WAITED) {
await sleep(LEDGER_CLOSE_TIME)
const currentBalance = await issuingClient.getXrpBalance(
wallet2.classicAddress,
)
console.log(initialBalance, currentBalance)
if (currentBalance > initialBalance) {
console.log('Transfer is complete')
console.log(
`New balance of ${wallet2.classicAddress} is ${currentBalance} XRP`,
)
break
}
ledgersWaited += 1
if (ledgersWaited === MAX_LEDGERS_WAITED) {
throw Error('Bridge transfer failed.')
}
}
await lockingClient.disconnect()
await issuingClient.disconnect()
}

View File

@@ -1,5 +1,11 @@
import { Client, Payment } from '../../src'
// Prerequisites for this snippet. Please verify these conditions after a reset of the
// test network:
// - destination_account must have a trust line with the destination_amount.issuer
// - There must be appropriate DEX Offers or XRP/TST AMM for the cross-currency exchange
// PathFind RPC requires the use of a Websocket client only
const client = new Client('wss://s.altnet.rippletest.net:51233')
async function createTxWithPaths(): Promise<void> {
@@ -8,22 +14,17 @@ async function createTxWithPaths(): Promise<void> {
const { wallet } = await client.fundWallet(null, {
usageContext: 'code snippets',
})
const destination_account = 'rKT4JX4cCof6LcDYRz8o3rGRu7qxzZ2Zwj'
const destination_account = 'rJPeZVPty1bXXbDR9oKscg2irqABr7sP3t'
const destination_amount = {
value: '0.001',
currency: 'USD',
issuer: 'rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc',
currency: 'TST',
issuer: 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd',
}
const resp = await client.request({
// TOOD: Replace with path_find - https://github.com/XRPLF/xrpl.js/issues/2385
command: 'ripple_path_find',
command: 'path_find',
subcommand: 'create',
source_account: wallet.classicAddress,
source_currencies: [
{
currency: 'XRP',
},
],
destination_account,
destination_amount,
})

View File

@@ -4,22 +4,35 @@ import { decode } from 'ripple-binary-codec'
import type {
TransactionEntryResponse,
TransactionStream,
TransactionV1Stream,
TxResponse,
} from '..'
import type { Amount, APIVersion, DEFAULT_API_VERSION } from '../models/common'
import type {
Amount,
IssuedCurrency,
APIVersion,
DEFAULT_API_VERSION,
MPTAmount,
} from '../models/common'
import type {
AccountTxTransaction,
RequestResponseMap,
} from '../models/methods'
import { AccountTxVersionResponseMap } from '../models/methods/accountTx'
import { BaseRequest, BaseResponse } from '../models/methods/baseMethod'
import { PaymentFlags, Transaction } from '../models/transactions'
import { PaymentFlags, Transaction, isMPTAmount } from '../models/transactions'
import type { TransactionMetadata } from '../models/transactions/metadata'
import { isFlagEnabled } from '../models/utils'
const WARN_PARTIAL_PAYMENT_CODE = 2001
function amountsEqual(amt1: Amount, amt2: Amount): boolean {
/* eslint-disable complexity -- check different token types */
/* eslint-disable @typescript-eslint/consistent-type-assertions -- known currency type */
function amountsEqual(
amt1: Amount | MPTAmount,
amt2: Amount | MPTAmount,
): boolean {
// Compare XRP
if (typeof amt1 === 'string' && typeof amt2 === 'string') {
return amt1 === amt2
}
@@ -28,15 +41,32 @@ function amountsEqual(amt1: Amount, amt2: Amount): boolean {
return false
}
// Compare MPTs
if (isMPTAmount(amt1) && isMPTAmount(amt2)) {
const aValue = new BigNumber(amt1.value)
const bValue = new BigNumber(amt2.value)
return (
amt1.mpt_issuance_id === amt2.mpt_issuance_id && aValue.isEqualTo(bValue)
)
}
if (isMPTAmount(amt1) || isMPTAmount(amt2)) {
return false
}
// Compare issued currency (IOU)
const aValue = new BigNumber(amt1.value)
const bValue = new BigNumber(amt2.value)
return (
amt1.currency === amt2.currency &&
amt1.issuer === amt2.issuer &&
(amt1 as IssuedCurrency).currency === (amt2 as IssuedCurrency).currency &&
(amt1 as IssuedCurrency).issuer === (amt2 as IssuedCurrency).issuer &&
aValue.isEqualTo(bValue)
)
}
/* eslint-enable complexity */
/* eslint-enable @typescript-eslint/consistent-type-assertions */
function isPartialPayment(
tx?: Transaction,
@@ -159,10 +189,10 @@ export function handlePartialPayment<
* @param log - The method used for logging by the connection (to report the partial payment).
*/
export function handleStreamPartialPayment(
stream: TransactionStream,
stream: TransactionStream | TransactionV1Stream,
log: (id: string, message: string) => void,
): void {
if (isPartialPayment(stream.transaction, stream.meta)) {
if (isPartialPayment(stream.tx_json ?? stream.transaction, stream.meta)) {
const warnings = stream.warnings ?? []
const warning = {

View File

@@ -20,6 +20,11 @@ export interface IssuedCurrencyAmount extends IssuedCurrency {
value: string
}
export interface MPTAmount {
mpt_issuance_id: string
value: string
}
export type Amount = IssuedCurrencyAmount | string
export interface Balance {
@@ -157,6 +162,16 @@ export interface AuthAccount {
}
}
export interface AuthorizeCredential {
Credential: {
/** The issuer of the credential. */
Issuer: string
/** A hex-encoded value to identify the type of credential from the issuer. */
CredentialType: string
}
}
export interface XChainBridge {
LockingChainDoor: string
LockingChainIssue: Currency

View File

@@ -10,6 +10,7 @@ export * as LedgerEntry from './ledger'
export {
setTransactionFlagsToNumber,
parseAccountRootFlags,
parseTransactionFlags,
} from './utils/flags'
export * from './methods'
export * from './transactions'

View File

@@ -0,0 +1,47 @@
import { GlobalFlags } from '../transactions/common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface CredentialFlags extends GlobalFlags {
lsfAccepted?: boolean
}
/**
*
* A Credential object describes a credential, similar to a passport, which is an issuable identity verifier
* that can be used as a prerequisite for other transactions
*
* @category Ledger Entries
*/
export default interface Credential extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'Credential'
/**
* A bit-map of boolean flags
*/
Flags: number | CredentialFlags
/** The account that the credential is for. */
Subject: string
/** The issuer of the credential. */
Issuer: string
/** A hex-encoded value to identify the type of credential from the issuer. */
CredentialType: string
/** A hint indicating which page of the subject's owner directory links to this object,
* in case the directory consists of multiple pages.
*/
SubjectNode: string
/** A hint indicating which page of the issuer's owner directory links to this object,
* in case the directory consists of multiple pages.
*/
IssuerNode: string
/** Credential expiration. */
Expiration?: number
/** Additional data about the credential (such as a link to the VC document). */
URI?: string
}

View File

@@ -1,3 +1,5 @@
import { AuthorizeCredential } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
/**
@@ -12,8 +14,6 @@ export default interface DepositPreauth
LedgerEntryType: 'DepositPreauth'
/** The account that granted the preauthorization. */
Account: string
/** The account that received the preauthorization. */
Authorize: string
/**
* A bit-map of boolean flags. No flags are defined for DepositPreauth
* objects, so this value is always 0.
@@ -24,4 +24,8 @@ export default interface DepositPreauth
* object, in case the directory consists of multiple pages.
*/
OwnerNode: string
/** The account that received the preauthorization. */
Authorize?: string
/** The credential(s) that received the preauthorization. */
AuthorizeCredentials?: AuthorizeCredential[]
}

View File

@@ -31,6 +31,11 @@ interface BaseLedger {
* by which the close_time could be rounded.
*/
close_time_resolution: number
/**
* The approximate time this ledger was closed, in date time string format.
* Always uses the UTC time zone.
*/
close_time_iso: string
/** Whether or not this ledger has been closed. */
closed: boolean
/**

View File

@@ -3,6 +3,7 @@ import Amendments from './Amendments'
import AMM from './AMM'
import Bridge from './Bridge'
import Check from './Check'
import Credential from './Credential'
import DepositPreauth from './DepositPreauth'
import DirectoryNode from './DirectoryNode'
import Escrow from './Escrow'
@@ -24,6 +25,7 @@ type LedgerEntry =
| AMM
| Bridge
| Check
| Credential
| DepositPreauth
| DirectoryNode
| Escrow
@@ -45,12 +47,15 @@ type LedgerEntryFilter =
| 'amm'
| 'bridge'
| 'check'
| 'credential'
| 'deposit_preauth'
| 'did'
| 'directory'
| 'escrow'
| 'fee'
| 'hashes'
| 'mpt_issuance'
| 'mptoken'
| 'nft_offer'
| 'nft_page'
| 'offer'

View File

@@ -0,0 +1,11 @@
import { MPTAmount } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface MPToken extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'MPToken'
MPTokenIssuanceID: string
MPTAmount?: MPTAmount
Flags: number
OwnerNode?: string
}

View File

@@ -0,0 +1,13 @@
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface MPTokenIssuance extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'MPTokenIssuance'
Flags: number
Issuer: string
AssetScale?: number
MaximumAmount?: string
OutstandingAmount: string
TransferFee?: number
MPTokenMetadata?: string
OwnerNode?: string
}

View File

@@ -6,6 +6,7 @@ import Amendments, { Majority, AMENDMENTS_ID } from './Amendments'
import AMM, { VoteSlot } from './AMM'
import Bridge from './Bridge'
import Check from './Check'
import Credential from './Credential'
import DepositPreauth from './DepositPreauth'
import DID from './DID'
import DirectoryNode from './DirectoryNode'
@@ -18,6 +19,8 @@ import FeeSettings, {
import { Ledger, LedgerV1 } from './Ledger'
import { LedgerEntry, LedgerEntryFilter } from './LedgerEntry'
import LedgerHashes from './LedgerHashes'
import { MPToken } from './MPToken'
import { MPTokenIssuance } from './MPTokenIssuance'
import NegativeUNL, { NEGATIVE_UNL_ID } from './NegativeUNL'
import { NFTokenOffer } from './NFTokenOffer'
import { NFToken, NFTokenPage } from './NFTokenPage'
@@ -39,6 +42,7 @@ export {
AMM,
Bridge,
Check,
Credential,
DepositPreauth,
DirectoryNode,
DID,
@@ -55,6 +59,8 @@ export {
Majority,
NEGATIVE_UNL_ID,
NegativeUNL,
MPTokenIssuance,
MPToken,
NFTokenOffer,
NFTokenPage,
NFToken,

View File

@@ -15,6 +15,12 @@ export interface DepositAuthorizedRequest
source_account: string
/** The recipient of a possible payment. */
destination_account: string
/**
* The object IDs of Credential objects. If this field is included, then the
* credential will be taken into account when analyzing whether the sender can send
* funds to the destination.
*/
credentials?: string[]
}
/**
@@ -52,5 +58,9 @@ export interface DepositAuthorizedResponse extends BaseResponse {
source_account: string
/** If true, the information comes from a validated ledger version. */
validated?: boolean
/** The object IDs of `Credential` objects. If this field is included,
* then the credential will be taken into account when analyzing whether
* the sender can send funds to the destination. */
credentials?: string[]
}
}

View File

@@ -168,6 +168,7 @@ import {
SubscribeRequest,
SubscribeResponse,
TransactionStream,
TransactionV1Stream,
ValidationStream,
} from './subscribe'
import {
@@ -583,6 +584,7 @@ export {
LedgerStreamResponse,
ValidationStream,
TransactionStream,
TransactionV1Stream,
PathFindStream,
PeerStatusStream,
OrderBookStream,

View File

@@ -21,6 +21,22 @@ import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod'
*/
export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
command: 'ledger_entry'
/**
* Retrieve a MPTokenIssuance object from the ledger.
*/
mpt_issuance?: string
/**
* Retrieve a MPToken object from the ledger.
*/
mptoken?:
| {
mpt_issuance_id: string
account: string
}
| string
/**
* Retrieve an Automated Market Maker (AMM) object from the ledger.
* This is similar to amm_info method, but the ledger_entry version returns only the ledger entry as stored.
@@ -35,7 +51,14 @@ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
issuer?: string
}
}
/**
* (Optional) If set to true and the queried object has been deleted,
* return its complete data prior to its deletion.
* If set to false or not provided and the queried object has been deleted,
* return objectNotFound (current behavior).
* This parameter is supported only by Clio servers
*/
include_deleted?: boolean
/**
* If true, return the requested ledger object's contents as a hex string in
* the XRP Ledger's binary format. Otherwise, return data in JSON format. The
@@ -60,6 +83,23 @@ export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
/** The object ID of a Check object to retrieve. */
check?: string
/* Specify the Credential to retrieve. If a string, must be the ledger entry ID of
* the entry, as hexadecimal. If an object, requires subject, issuer, and
* credential_type sub-fields.
*/
credential?:
| {
/** The account that is the subject of the credential. */
subject: string
/** The account that issued the credential. */
issuer: string
/** The type of the credential, as issued. */
credentialType: string
}
| string
/**
* Specify a DepositPreauth object to retrieve. If a string, must be the
* object ID of the DepositPreauth object, as hexadecimal. If an object,
@@ -204,5 +244,9 @@ export interface LedgerEntryResponse<T = LedgerEntry> extends BaseResponse {
/** The binary representation of the ledger object, as hexadecimal. */
node_binary?: string
validated?: boolean
/**
* (Optional) Indicates the ledger index at which the object was deleted.
*/
deleted_ledger_index?: number
}
}

View File

@@ -4,6 +4,10 @@ import type {
Path,
StreamType,
ResponseOnlyTxInfo,
APIVersion,
DEFAULT_API_VERSION,
RIPPLED_API_V1,
RIPPLED_API_V2,
} from '../common'
import { Offer } from '../ledger'
import { OfferCreate, Transaction } from '../transactions'
@@ -262,9 +266,16 @@ export interface ValidationStream extends BaseStream {
*
* @category Streams
*/
export interface TransactionStream extends BaseStream {
interface TransactionStreamBase<
Version extends APIVersion = typeof DEFAULT_API_VERSION,
> extends BaseStream {
status: string
type: 'transaction'
/**
* The approximate time this ledger was closed, in date time string format.
* Always uses the UTC time zone.
*/
close_time_iso: string
/** String Transaction result code. */
engine_result: string
/** Numeric transaction response code, if applicable. */
@@ -285,8 +296,14 @@ export interface TransactionStream extends BaseStream {
* in detail.
*/
meta?: TransactionMetadata
/** The definition of the transaction in JSON format. */
transaction: Transaction & ResponseOnlyTxInfo
/** JSON object defining the transaction. */
tx_json?: Version extends typeof RIPPLED_API_V2
? Transaction & ResponseOnlyTxInfo
: never
/** JSON object defining the transaction in rippled API v1. */
transaction?: Version extends typeof RIPPLED_API_V1
? Transaction & ResponseOnlyTxInfo
: never
/**
* If true, this transaction is included in a validated ledger and its
* outcome is final. Responses from the transaction stream should always be
@@ -296,6 +313,20 @@ export interface TransactionStream extends BaseStream {
warnings?: Array<{ id: number; message: string }>
}
/**
* Expected response from an {@link AccountTxRequest}.
*
* @category Streams
*/
export type TransactionStream = TransactionStreamBase
/**
* Expected response from an {@link AccountTxRequest} with `api_version` set to 1.
*
* @category Streams
*/
export type TransactionV1Stream = TransactionStreamBase<typeof RIPPLED_API_V1>
/**
* The admin-only `peer_status` stream reports a large amount of information on
* the activities of other rippled servers to which this server is connected, in

View File

@@ -0,0 +1,44 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateCredentialType,
validateRequiredField,
} from './common'
/**
* Accepts a credential issued to the Account (i.e. the Account is the Subject of the Credential object).
* Credentials are represented in hex. Whilst they are allowed a maximum length of 64
* bytes, every byte requires 2 hex characters for representation.
* The credential is not considered valid until it has been transferred/accepted.
*
* @category Transaction Models
* */
export interface CredentialAccept extends BaseTransaction {
TransactionType: 'CredentialAccept'
/** The subject of the credential. */
Account: string
/** The issuer of the credential. */
Issuer: string
/** A hex-encoded value to identify the type of credential from the issuer. */
CredentialType: string
}
/**
* Verify the form and type of a CredentialAccept at runtime.
*
* @param tx - A CredentialAccept Transaction.
* @throws When the CredentialAccept is Malformed.
*/
export function validateCredentialAccept(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'Account', isString)
validateRequiredField(tx, 'Issuer', isString)
validateCredentialType(tx)
}

View File

@@ -0,0 +1,81 @@
import { HEX_REGEX } from '@xrplf/isomorphic/utils'
import { ValidationError } from '../../errors'
import {
BaseTransaction,
isNumber,
isString,
validateBaseTransaction,
validateCredentialType,
validateOptionalField,
validateRequiredField,
} from './common'
const MAX_URI_LENGTH = 256
/**
* Creates a Credential object. It must be sent by the issuer.
*
* @category Transaction Models
* */
export interface CredentialCreate extends BaseTransaction {
TransactionType: 'CredentialCreate'
/** The issuer of the credential. */
Account: string
/** The subject of the credential. */
Subject: string
/** A hex-encoded value to identify the type of credential from the issuer. */
CredentialType: string
/** Credential expiration. */
Expiration?: number
/** Additional data about the credential (such as a link to the VC document). */
URI?: string
}
/**
* Verify the form and type of a CredentialCreate at runtime.
*
* @param tx - A CredentialCreate Transaction.
* @throws When the CredentialCreate is Malformed.
*/
export function validateCredentialCreate(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'Account', isString)
validateRequiredField(tx, 'Subject', isString)
validateCredentialType(tx)
validateOptionalField(tx, 'Expiration', isNumber)
validateURI(tx.URI)
}
function validateURI(URI: unknown): void {
if (URI === undefined) {
return
}
if (typeof URI !== 'string') {
throw new ValidationError('CredentialCreate: invalid field URI')
}
if (URI.length === 0) {
throw new ValidationError('CredentialCreate: URI cannot be an empty string')
} else if (URI.length > MAX_URI_LENGTH) {
throw new ValidationError(
`CredentialCreate: URI length must be <= ${MAX_URI_LENGTH}`,
)
}
if (!HEX_REGEX.test(URI)) {
throw new ValidationError('CredentialCreate: URI must be encoded in hex')
}
}

View File

@@ -0,0 +1,55 @@
import { ValidationError } from '../../errors'
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateCredentialType,
validateOptionalField,
validateRequiredField,
} from './common'
/**
* Deletes a Credential object.
*
* @category Transaction Models
* */
export interface CredentialDelete extends BaseTransaction {
TransactionType: 'CredentialDelete'
/** The transaction submitter. */
Account: string
/** A hex-encoded value to identify the type of credential from the issuer. */
CredentialType: string
/** The person that the credential is for. If omitted, Account is assumed to be the subject. */
Subject?: string
/** The issuer of the credential. If omitted, Account is assumed to be the issuer. */
Issuer?: string
}
/**
* Verify the form and type of a CredentialDelete at runtime.
*
* @param tx - A CredentialDelete Transaction.
* @throws When the CredentialDelete is Malformed.
*/
export function validateCredentialDelete(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (!tx.Subject && !tx.Issuer) {
throw new ValidationError(
'CredentialDelete: either `Issuer` or `Subject` must be provided',
)
}
validateRequiredField(tx, 'Account', isString)
validateCredentialType(tx)
validateOptionalField(tx, 'Subject', isString)
validateOptionalField(tx, 'Issuer', isString)
}

View File

@@ -0,0 +1,67 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
Account,
validateOptionalField,
isAccount,
GlobalFlags,
} from './common'
/**
* Transaction Flags for an MPTokenAuthorize Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenAuthorizeFlags {
/**
* If set and transaction is submitted by a holder, it indicates that the holder no
* longer wants to hold the MPToken, which will be deleted as a result. If the the holder's
* MPToken has non-zero balance while trying to set this flag, the transaction will fail. On
* the other hand, if set and transaction is submitted by an issuer, it would mean that the
* issuer wants to unauthorize the holder (only applicable for allow-listing),
* which would unset the lsfMPTAuthorized flag on the MPToken.
*/
tfMPTUnauthorize = 0x00000001,
}
/**
* Map of flags to boolean values representing {@link MPTokenAuthorize} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenAuthorizeFlagsInterface extends GlobalFlags {
tfMPTUnauthorize?: boolean
}
/**
* The MPTokenAuthorize transaction is used to globally lock/unlock a MPTokenIssuance,
* or lock/unlock an individual's MPToken.
*/
export interface MPTokenAuthorize extends BaseTransaction {
TransactionType: 'MPTokenAuthorize'
/**
* Identifies the MPTokenIssuance
*/
MPTokenIssuanceID: string
/**
* An optional XRPL Address of an individual token holder balance to lock/unlock.
* If omitted, this transaction will apply to all any accounts holding MPTs.
*/
Holder?: Account
Flags?: number | MPTokenAuthorizeFlagsInterface
}
/**
* Verify the form and type of an MPTokenAuthorize at runtime.
*
* @param tx - An MPTokenAuthorize Transaction.
* @throws When the MPTokenAuthorize is Malformed.
*/
export function validateMPTokenAuthorize(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
validateOptionalField(tx, 'Holder', isAccount)
}

View File

@@ -0,0 +1,179 @@
import { ValidationError } from '../../errors'
import { isHex, INTEGER_SANITY_CHECK, isFlagEnabled } from '../utils'
import {
BaseTransaction,
GlobalFlags,
validateBaseTransaction,
validateOptionalField,
isString,
isNumber,
} from './common'
import type { TransactionMetadataBase } from './metadata'
// 2^63 - 1
const MAX_AMT = '9223372036854775807'
const MAX_TRANSFER_FEE = 50000
/**
* Transaction Flags for an MPTokenIssuanceCreate Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenIssuanceCreateFlags {
/**
* If set, indicates that the MPT can be locked both individually and globally.
* If not set, the MPT cannot be locked in any way.
*/
tfMPTCanLock = 0x00000002,
/**
* If set, indicates that individual holders must be authorized.
* This enables issuers to limit who can hold their assets.
*/
tfMPTRequireAuth = 0x00000004,
/**
* If set, indicates that individual holders can place their balances into an escrow.
*/
tfMPTCanEscrow = 0x00000008,
/**
* If set, indicates that individual holders can trade their balances
* using the XRP Ledger DEX or AMM.
*/
tfMPTCanTrade = 0x00000010,
/**
* If set, indicates that tokens may be transferred to other accounts
* that are not the issuer.
*/
tfMPTCanTransfer = 0x00000020,
/**
* If set, indicates that the issuer may use the Clawback transaction
* to clawback value from individual holders.
*/
tfMPTCanClawback = 0x00000040,
}
/**
* Map of flags to boolean values representing {@link MPTokenIssuanceCreate} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenIssuanceCreateFlagsInterface extends GlobalFlags {
tfMPTCanLock?: boolean
tfMPTRequireAuth?: boolean
tfMPTCanEscrow?: boolean
tfMPTCanTrade?: boolean
tfMPTCanTransfer?: boolean
tfMPTCanClawback?: boolean
}
/**
* The MPTokenIssuanceCreate transaction creates a MPTokenIssuance object
* and adds it to the relevant directory node of the creator account.
* This transaction is the only opportunity an issuer has to specify any token fields
* that are defined as immutable (e.g., MPT Flags). If the transaction is successful,
* the newly created token will be owned by the account (the creator account) which
* executed the transaction.
*/
export interface MPTokenIssuanceCreate extends BaseTransaction {
TransactionType: 'MPTokenIssuanceCreate'
/**
* An asset scale is the difference, in orders of magnitude, between a standard unit and
* a corresponding fractional unit. More formally, the asset scale is a non-negative integer
* (0, 1, 2, …) such that one standard unit equals 10^(-scale) of a corresponding
* fractional unit. If the fractional unit equals the standard unit, then the asset scale is 0.
* Note that this value is optional, and will default to 0 if not supplied.
*/
AssetScale?: number
/**
* Specifies the maximum asset amount of this token that should ever be issued.
* It is a non-negative integer string that can store a range of up to 63 bits. If not set, the max
* amount will default to the largest unsigned 63-bit integer (0x7FFFFFFFFFFFFFFF or 9223372036854775807)
*
* Example:
* ```
* MaximumAmount: '9223372036854775807'
* ```
*/
MaximumAmount?: string
/**
* Specifies the fee to charged by the issuer for secondary sales of the Token,
* if such sales are allowed. Valid values for this field are between 0 and 50,000 inclusive,
* allowing transfer rates of between 0.000% and 50.000% in increments of 0.001.
* The field must NOT be present if the `tfMPTCanTransfer` flag is not set.
*/
TransferFee?: number
/**
* Arbitrary metadata about this issuance, in hex format.
*/
MPTokenMetadata?: string | null
Flags?: number | MPTokenIssuanceCreateFlagsInterface
}
export interface MPTokenIssuanceCreateMetadata extends TransactionMetadataBase {
mpt_issuance_id?: string
}
/* eslint-disable max-lines-per-function -- Not needed to reduce function */
/**
* Verify the form and type of an MPTokenIssuanceCreate at runtime.
*
* @param tx - An MPTokenIssuanceCreate Transaction.
* @throws When the MPTokenIssuanceCreate is Malformed.
*/
export function validateMPTokenIssuanceCreate(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'MaximumAmount', isString)
validateOptionalField(tx, 'MPTokenMetadata', isString)
validateOptionalField(tx, 'TransferFee', isNumber)
validateOptionalField(tx, 'AssetScale', isNumber)
if (typeof tx.MPTokenMetadata === 'string' && tx.MPTokenMetadata === '') {
throw new ValidationError(
'MPTokenIssuanceCreate: MPTokenMetadata must not be empty string',
)
}
if (typeof tx.MPTokenMetadata === 'string' && !isHex(tx.MPTokenMetadata)) {
throw new ValidationError(
'MPTokenIssuanceCreate: MPTokenMetadata must be in hex format',
)
}
if (typeof tx.MaximumAmount === 'string') {
if (!INTEGER_SANITY_CHECK.exec(tx.MaximumAmount)) {
throw new ValidationError('MPTokenIssuanceCreate: Invalid MaximumAmount')
} else if (
BigInt(tx.MaximumAmount) > BigInt(MAX_AMT) ||
BigInt(tx.MaximumAmount) < BigInt(`0`)
) {
throw new ValidationError(
'MPTokenIssuanceCreate: MaximumAmount out of range',
)
}
}
if (typeof tx.TransferFee === 'number') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Not necessary
const flags = tx.Flags as number | MPTokenIssuanceCreateFlagsInterface
const isTfMPTCanTransfer =
typeof flags === 'number'
? isFlagEnabled(flags, MPTokenIssuanceCreateFlags.tfMPTCanTransfer)
: flags.tfMPTCanTransfer ?? false
if (tx.TransferFee < 0 || tx.TransferFee > MAX_TRANSFER_FEE) {
throw new ValidationError(
`MPTokenIssuanceCreate: TransferFee must be between 0 and ${MAX_TRANSFER_FEE}`,
)
}
if (tx.TransferFee && !isTfMPTCanTransfer) {
throw new ValidationError(
'MPTokenIssuanceCreate: TransferFee cannot be provided without enabling tfMPTCanTransfer flag',
)
}
}
}
/* eslint-enable max-lines-per-function */

View File

@@ -0,0 +1,34 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* The MPTokenIssuanceDestroy transaction is used to remove an MPTokenIssuance object
* from the directory node in which it is being held, effectively removing the token
* from the ledger. If this operation succeeds, the corresponding
* MPTokenIssuance is removed and the owners reserve requirement is reduced by one.
* This operation must fail if there are any holders who have non-zero balances.
*/
export interface MPTokenIssuanceDestroy extends BaseTransaction {
TransactionType: 'MPTokenIssuanceDestroy'
/**
* Identifies the MPTokenIssuance object to be removed by the transaction.
*/
MPTokenIssuanceID: string
}
/**
* Verify the form and type of an MPTokenIssuanceDestroy at runtime.
*
* @param tx - An MPTokenIssuanceDestroy Transaction.
* @throws When the MPTokenIssuanceDestroy is Malformed.
*/
export function validateMPTokenIssuanceDestroy(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
}

View File

@@ -0,0 +1,86 @@
import { ValidationError } from '../../errors'
import { isFlagEnabled } from '../utils'
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
Account,
validateOptionalField,
isAccount,
GlobalFlags,
} from './common'
/**
* Transaction Flags for an MPTokenIssuanceSet Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenIssuanceSetFlags {
/**
* If set, indicates that issuer locks the MPT
*/
tfMPTLock = 0x00000001,
/**
* If set, indicates that issuer unlocks the MPT
*/
tfMPTUnlock = 0x00000002,
}
/**
* Map of flags to boolean values representing {@link MPTokenIssuanceSet} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenIssuanceSetFlagsInterface extends GlobalFlags {
tfMPTLock?: boolean
tfMPTUnlock?: boolean
}
/**
* The MPTokenIssuanceSet transaction is used to globally lock/unlock a MPTokenIssuance,
* or lock/unlock an individual's MPToken.
*/
export interface MPTokenIssuanceSet extends BaseTransaction {
TransactionType: 'MPTokenIssuanceSet'
/**
* Identifies the MPTokenIssuance
*/
MPTokenIssuanceID: string
/**
* An optional XRPL Address of an individual token holder balance to lock/unlock.
* If omitted, this transaction will apply to all any accounts holding MPTs.
*/
Holder?: Account
Flags?: number | MPTokenIssuanceSetFlagsInterface
}
/**
* Verify the form and type of an MPTokenIssuanceSet at runtime.
*
* @param tx - An MPTokenIssuanceSet Transaction.
* @throws When the MPTokenIssuanceSet is Malformed.
*/
export function validateMPTokenIssuanceSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
validateOptionalField(tx, 'Holder', isAccount)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Not necessary
const flags = tx.Flags as number | MPTokenIssuanceSetFlagsInterface
const isTfMPTLock =
typeof flags === 'number'
? isFlagEnabled(flags, MPTokenIssuanceSetFlags.tfMPTLock)
: flags.tfMPTLock ?? false
const isTfMPTUnlock =
typeof flags === 'number'
? isFlagEnabled(flags, MPTokenIssuanceSetFlags.tfMPTUnlock)
: flags.tfMPTUnlock ?? false
if (isTfMPTLock && isTfMPTUnlock) {
throw new ValidationError('MPTokenIssuanceSet: flag conflict')
}
}

View File

@@ -4,6 +4,7 @@ import {
isAccount,
isNumber,
validateBaseTransaction,
validateCredentialsList,
validateOptionalField,
validateRequiredField,
} from './common'
@@ -28,6 +29,12 @@ export interface AccountDelete extends BaseTransaction {
* information for the recipient of the deleted account's leftover XRP.
*/
DestinationTag?: number
/**
* Credentials associated with sender of this transaction. The credentials included
* must not be expired. The list must not be empty when specified and cannot contain
* more than 8 credentials.
*/
CredentialIDs?: string[]
}
/**
@@ -41,4 +48,11 @@ export function validateAccountDelete(tx: Record<string, unknown>): void {
validateRequiredField(tx, 'Destination', isAccount)
validateOptionalField(tx, 'DestinationTag', isNumber)
validateCredentialsList(
tx.CredentialIDs,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
)
}

View File

@@ -1,10 +1,13 @@
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount } from '../common'
import { IssuedCurrencyAmount, MPTAmount } from '../common'
import {
BaseTransaction,
validateBaseTransaction,
isIssuedCurrency,
isMPTAmount,
isAccount,
validateOptionalField,
} from './common'
/**
@@ -15,15 +18,20 @@ export interface Clawback extends BaseTransaction {
TransactionType: 'Clawback'
/**
* Indicates the AccountID that submitted this transaction. The account MUST
* be the issuer of the currency.
* be the issuer of the currency or MPT.
*/
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,
* The amount of currency or MPT to clawback, and it must be non-XRP. The nested field
* names MUST be lower-case. If the amount is IOU, the `issuer` field MUST be the holder's address,
* whom to be clawed back.
*/
Amount: IssuedCurrencyAmount
Amount: IssuedCurrencyAmount | MPTAmount
/**
* Indicates the AccountID that the issuer wants to clawback. This field is only valid for clawing back
* MPTs.
*/
Holder?: string
}
/**
@@ -34,16 +42,29 @@ export interface Clawback extends BaseTransaction {
*/
export function validateClawback(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'Holder', isAccount)
if (tx.Amount == null) {
throw new ValidationError('Clawback: missing field Amount')
}
if (!isIssuedCurrency(tx.Amount)) {
if (!isIssuedCurrency(tx.Amount) && !isMPTAmount(tx.Amount)) {
throw new ValidationError('Clawback: invalid Amount')
}
if (isIssuedCurrency(tx.Amount) && tx.Account === tx.Amount.issuer) {
throw new ValidationError('Clawback: invalid holder Account')
}
if (isMPTAmount(tx.Amount) && tx.Account === tx.Holder) {
throw new ValidationError('Clawback: invalid holder Account')
}
if (isIssuedCurrency(tx.Amount) && tx.Holder) {
throw new ValidationError('Clawback: cannot have Holder for currency')
}
if (isMPTAmount(tx.Amount) && !tx.Holder) {
throw new ValidationError('Clawback: missing Holder')
}
}

View File

@@ -1,18 +1,25 @@
/* eslint-disable max-lines -- common utility file */
import { HEX_REGEX } from '@xrplf/isomorphic/utils'
import { isValidClassicAddress, isValidXAddress } from 'ripple-address-codec'
import { TRANSACTION_TYPES } from 'ripple-binary-codec'
import { ValidationError } from '../../errors'
import {
Amount,
AuthorizeCredential,
Currency,
IssuedCurrencyAmount,
Memo,
Signer,
XChainBridge,
MPTAmount,
} from '../common'
import { onlyHasFields } from '../utils'
const MEMO_SIZE = 3
const MAX_CREDENTIALS_LIST_LENGTH = 8
const MAX_CREDENTIAL_BYTE_LENGTH = 64
const MAX_CREDENTIAL_TYPE_LENGTH = MAX_CREDENTIAL_BYTE_LENGTH * 2
function isMemo(obj: { Memo?: unknown }): boolean {
if (obj.Memo == null) {
@@ -59,6 +66,8 @@ const XRP_CURRENCY_SIZE = 1
const ISSUE_SIZE = 2
const ISSUED_CURRENCY_SIZE = 3
const XCHAIN_BRIDGE_SIZE = 4
const MPTOKEN_SIZE = 2
const AUTHORIZE_CREDENTIAL_SIZE = 1
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
@@ -119,6 +128,37 @@ export function isIssuedCurrency(
)
}
/**
* Verify the form and type of an AuthorizeCredential at runtime
*
* @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 {
return (
isRecord(input) &&
isRecord(input.Credential) &&
Object.keys(input).length === AUTHORIZE_CREDENTIAL_SIZE &&
typeof input.Credential.CredentialType === 'string' &&
typeof input.Credential.Issuer === 'string'
)
}
/**
* Verify the form and type of an MPT at runtime.
*
* @param input - The input to check the form and type of.
* @returns Whether the MPTAmount is properly formed.
*/
export function isMPTAmount(input: unknown): input is MPTAmount {
return (
isRecord(input) &&
Object.keys(input).length === MPTOKEN_SIZE &&
typeof input.value === 'string' &&
typeof input.mpt_issuance_id === 'string'
)
}
/**
* Must be a valid account address
*/
@@ -144,7 +184,11 @@ export function isAccount(account: unknown): account is Account {
* @returns Whether the Amount is properly formed.
*/
export function isAmount(amount: unknown): amount is Amount {
return typeof amount === 'string' || isIssuedCurrency(amount)
return (
typeof amount === 'string' ||
isIssuedCurrency(amount) ||
isMPTAmount(amount)
)
}
/**
@@ -366,3 +410,97 @@ export function parseAmountValue(amount: unknown): number {
}
return parseFloat(amount.value)
}
/**
* Verify the form and type of a CredentialType at runtime.
*
* @param tx A CredentialType Transaction.
* @throws when the CredentialType is malformed.
*/
export function validateCredentialType(tx: Record<string, unknown>): void {
if (typeof tx.TransactionType !== 'string') {
throw new ValidationError('Invalid TransactionType')
}
if (tx.CredentialType === undefined) {
throw new ValidationError(
`${tx.TransactionType}: missing field CredentialType`,
)
}
if (!isString(tx.CredentialType)) {
throw new ValidationError(
`${tx.TransactionType}: CredentialType must be a string`,
)
}
if (tx.CredentialType.length === 0) {
throw new ValidationError(
`${tx.TransactionType}: CredentialType cannot be an empty string`,
)
} else if (tx.CredentialType.length > MAX_CREDENTIAL_TYPE_LENGTH) {
throw new ValidationError(
`${tx.TransactionType}: CredentialType length cannot be > ${MAX_CREDENTIAL_TYPE_LENGTH}`,
)
}
if (!HEX_REGEX.test(tx.CredentialType)) {
throw new ValidationError(
`${tx.TransactionType}: CredentialType must be encoded in hex`,
)
}
}
/**
* Check a CredentialAuthorize array for parameter errors
*
* @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
* @throws Validation Error if the formatting is incorrect
*/
// eslint-disable-next-line max-lines-per-function -- separating logic further will add unnecessary complexity
export function validateCredentialsList(
credentials: unknown,
transactionType: string,
isStringID: boolean,
): void {
if (credentials == null) {
return
}
if (!Array.isArray(credentials)) {
throw new ValidationError(
`${transactionType}: Credentials must be an array`,
)
}
if (credentials.length > MAX_CREDENTIALS_LIST_LENGTH) {
throw new ValidationError(
`${transactionType}: Credentials length cannot exceed ${MAX_CREDENTIALS_LIST_LENGTH} elements`,
)
} else if (credentials.length === 0) {
throw new ValidationError(
`${transactionType}: Credentials cannot be an empty array`,
)
}
credentials.forEach((credential) => {
if (isStringID) {
if (!isString(credential)) {
throw new ValidationError(
`${transactionType}: Invalid Credentials ID list format`,
)
}
} else if (!isAuthorizeCredential(credential)) {
throw new ValidationError(
`${transactionType}: Invalid Credentials format`,
)
}
})
if (containsDuplicates(credentials)) {
throw new ValidationError(
`${transactionType}: Credentials cannot contain duplicate elements`,
)
}
}
function containsDuplicates(objectList: object[]): boolean {
const objSet = new Set(objectList.map((obj) => JSON.stringify(obj)))
return objSet.size !== objectList.length
}

View File

@@ -1,6 +1,11 @@
import { ValidationError } from '../../errors'
import { AuthorizeCredential } from '../common'
import { BaseTransaction, validateBaseTransaction } from './common'
import {
BaseTransaction,
validateBaseTransaction,
validateCredentialsList,
} from './common'
/**
* A DepositPreauth transaction gives another account pre-approval to deliver
@@ -18,6 +23,16 @@ export interface DepositPreauth extends BaseTransaction {
* revoked.
*/
Unauthorize?: string
/**
* The credential(s) to preauthorize.
*/
AuthorizeCredentials?: AuthorizeCredential[]
/**
* The credential(s) whose preauthorization should be revoked.
*/
UnauthorizeCredentials?: AuthorizeCredential[]
}
/**
@@ -29,17 +44,7 @@ export interface DepositPreauth extends BaseTransaction {
export function validateDepositPreauth(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.Authorize !== undefined && tx.Unauthorize !== undefined) {
throw new ValidationError(
"DepositPreauth: can't provide both Authorize and Unauthorize fields",
)
}
if (tx.Authorize === undefined && tx.Unauthorize === undefined) {
throw new ValidationError(
'DepositPreauth: must provide either Authorize or Unauthorize field',
)
}
validateSingleAuthorizationFieldProvided(tx)
if (tx.Authorize !== undefined) {
if (typeof tx.Authorize !== 'string') {
@@ -51,9 +56,7 @@ export function validateDepositPreauth(tx: Record<string, unknown>): void {
"DepositPreauth: Account can't preauthorize its own address",
)
}
}
if (tx.Unauthorize !== undefined) {
} else if (tx.Unauthorize !== undefined) {
if (typeof tx.Unauthorize !== 'string') {
throw new ValidationError('DepositPreauth: Unauthorize must be a string')
}
@@ -63,5 +66,38 @@ export function validateDepositPreauth(tx: Record<string, unknown>): void {
"DepositPreauth: Account can't unauthorize its own address",
)
}
} else if (tx.AuthorizeCredentials !== undefined) {
validateCredentialsList(
tx.AuthorizeCredentials,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- confirmed in base transaction check
tx.TransactionType as string,
false,
)
} else if (tx.UnauthorizeCredentials !== undefined) {
validateCredentialsList(
tx.UnauthorizeCredentials,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- confirmed in base transaction check
tx.TransactionType as string,
false,
)
}
}
// Boolean logic to ensure exactly one of 4 inputs was provided
function validateSingleAuthorizationFieldProvided(
tx: Record<string, unknown>,
): void {
const fields = [
'Authorize',
'Unauthorize',
'AuthorizeCredentials',
'UnauthorizeCredentials',
]
const countProvided = fields.filter((key) => tx[key] !== undefined).length
if (countProvided !== 1) {
throw new ValidationError(
'DepositPreauth: Requires exactly one field of the following: Authorize, Unauthorize, AuthorizeCredentials, UnauthorizeCredentials.',
)
}
}

View File

@@ -5,6 +5,7 @@ import {
BaseTransaction,
isAccount,
validateBaseTransaction,
validateCredentialsList,
validateRequiredField,
} from './common'
@@ -32,6 +33,10 @@ export interface EscrowFinish extends BaseTransaction {
* the held payment's Condition.
*/
Fulfillment?: string
/** Credentials associated with the sender of this transaction.
* The credentials included must not be expired.
*/
CredentialIDs?: string[]
}
/**
@@ -45,6 +50,13 @@ export function validateEscrowFinish(tx: Record<string, unknown>): void {
validateRequiredField(tx, 'Owner', isAccount)
validateCredentialsList(
tx.CredentialIDs,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
)
if (tx.OfferSequence == null) {
throw new ValidationError('EscrowFinish: missing field OfferSequence')
}

View File

@@ -1,4 +1,4 @@
export { BaseTransaction } from './common'
export { BaseTransaction, isMPTAmount } from './common'
export {
validate,
PseudoTransaction,
@@ -32,6 +32,9 @@ export { CheckCancel } from './checkCancel'
export { CheckCash } from './checkCash'
export { CheckCreate } from './checkCreate'
export { Clawback } from './clawback'
export { CredentialAccept } from './CredentialAccept'
export { CredentialCreate } from './CredentialCreate'
export { CredentialDelete } from './CredentialDelete'
export { DIDDelete } from './DIDDelete'
export { DIDSet } from './DIDSet'
export { DepositPreauth } from './depositPreauth'
@@ -39,6 +42,22 @@ export { EscrowCancel } from './escrowCancel'
export { EscrowCreate } from './escrowCreate'
export { EscrowFinish } from './escrowFinish'
export { EnableAmendment, EnableAmendmentFlags } from './enableAmendment'
export {
MPTokenAuthorize,
MPTokenAuthorizeFlags,
MPTokenAuthorizeFlagsInterface,
} from './MPTokenAuthorize'
export {
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlags,
MPTokenIssuanceCreateFlagsInterface,
} from './MPTokenIssuanceCreate'
export { MPTokenIssuanceDestroy } from './MPTokenIssuanceDestroy'
export {
MPTokenIssuanceSet,
MPTokenIssuanceSetFlags,
MPTokenIssuanceSetFlagsInterface,
} from './MPTokenIssuanceSet'
export { NFTokenAcceptOffer } from './NFTokenAcceptOffer'
export { NFTokenBurn } from './NFTokenBurn'
export { NFTokenCancelOffer } from './NFTokenCancelOffer'

View File

@@ -1,6 +1,10 @@
import { Amount } from '../common'
import { Amount, MPTAmount } from '../common'
import { BaseTransaction } from './common'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceCreateMetadata,
} from './MPTokenIssuanceCreate'
import {
NFTokenAcceptOffer,
NFTokenAcceptOfferMetadata,
@@ -79,9 +83,9 @@ export function isDeletedNode(node: Node): node is DeletedNode {
export interface TransactionMetadataBase {
AffectedNodes: Node[]
DeliveredAmount?: Amount
DeliveredAmount?: Amount | MPTAmount
// "unavailable" possible for transactions before 2014-01-20
delivered_amount?: Amount | 'unavailable'
delivered_amount?: Amount | MPTAmount | 'unavailable'
TransactionIndex: number
TransactionResult: string
}
@@ -97,4 +101,6 @@ export type TransactionMetadata<T extends BaseTransaction = Transaction> =
? NFTokenAcceptOfferMetadata
: T extends NFTokenCancelOffer
? NFTokenCancelOfferMetadata
: T extends MPTokenIssuanceCreate
? MPTokenIssuanceCreateMetadata
: TransactionMetadataBase

View File

@@ -1,5 +1,5 @@
import { ValidationError } from '../../errors'
import { Amount, Path } from '../common'
import { Amount, Path, MPTAmount } from '../common'
import { isFlagEnabled } from '../utils'
import {
@@ -12,6 +12,7 @@ import {
validateOptionalField,
isNumber,
Account,
validateCredentialsList,
} from './common'
import type { TransactionMetadataBase } from './metadata'
@@ -116,7 +117,7 @@ export interface Payment extends BaseTransaction {
* names MUST be lower-case. If the tfPartialPayment flag is set, deliver up
* to this amount instead.
*/
Amount: Amount
Amount: Amount | MPTAmount
/** The unique address of the account receiving the payment. */
Destination: Account
/**
@@ -142,19 +143,24 @@ export interface Payment extends BaseTransaction {
* cross-currency/cross-issue payments. Must be omitted for XRP-to-XRP
* Payments.
*/
SendMax?: Amount
SendMax?: Amount | MPTAmount
/**
* Minimum amount of destination currency this transaction should deliver.
* Only valid if this is a partial payment. For non-XRP amounts, the nested
* field names are lower-case.
*/
DeliverMin?: Amount
DeliverMin?: Amount | MPTAmount
/**
* Credentials associated with the sender of this transaction.
* The credentials included must not be expired.
*/
CredentialIDs?: string[]
Flags?: number | PaymentFlagsInterface
}
export interface PaymentMetadata extends TransactionMetadataBase {
DeliveredAmount?: Amount
delivered_amount?: Amount | 'unavailable'
DeliveredAmount?: Amount | MPTAmount
delivered_amount?: Amount | MPTAmount | 'unavailable'
}
/**
@@ -177,6 +183,13 @@ export function validatePayment(tx: Record<string, unknown>): void {
validateRequiredField(tx, 'Destination', isAccount)
validateOptionalField(tx, 'DestinationTag', isNumber)
validateCredentialsList(
tx.CredentialIDs,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
)
if (tx.InvoiceID !== undefined && typeof tx.InvoiceID !== 'string') {
throw new ValidationError('PaymentTransaction: InvoiceID must be a string')
}

View File

@@ -1,6 +1,11 @@
import { ValidationError } from '../../errors'
import { BaseTransaction, GlobalFlags, validateBaseTransaction } from './common'
import {
BaseTransaction,
GlobalFlags,
validateBaseTransaction,
validateCredentialsList,
} from './common'
/**
* Enum representing values for PaymentChannelClaim transaction flags.
@@ -127,6 +132,11 @@ export interface PaymentChannelClaim extends BaseTransaction {
* field is omitted.
*/
PublicKey?: string
/**
* Credentials associated with the sender of this transaction.
* The credentials included must not be expired.
*/
CredentialIDs?: string[]
}
/**
@@ -138,6 +148,13 @@ export interface PaymentChannelClaim extends BaseTransaction {
export function validatePaymentChannelClaim(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateCredentialsList(
tx.CredentialIDs,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- known from base check
tx.TransactionType as string,
true,
)
if (tx.Channel === undefined) {
throw new ValidationError('PaymentChannelClaim: missing Channel')
}

View File

@@ -19,6 +19,9 @@ import { CheckCash, validateCheckCash } from './checkCash'
import { CheckCreate, validateCheckCreate } from './checkCreate'
import { Clawback, validateClawback } from './clawback'
import { BaseTransaction, isIssuedCurrency } from './common'
import { CredentialAccept, validateCredentialAccept } from './CredentialAccept'
import { CredentialCreate, validateCredentialCreate } from './CredentialCreate'
import { CredentialDelete, validateCredentialDelete } from './CredentialDelete'
import { DepositPreauth, validateDepositPreauth } from './depositPreauth'
import { DIDDelete, validateDIDDelete } from './DIDDelete'
import { DIDSet, validateDIDSet } from './DIDSet'
@@ -27,6 +30,19 @@ import { EscrowCancel, validateEscrowCancel } from './escrowCancel'
import { EscrowCreate, validateEscrowCreate } from './escrowCreate'
import { EscrowFinish, validateEscrowFinish } from './escrowFinish'
import { TransactionMetadata } from './metadata'
import { MPTokenAuthorize, validateMPTokenAuthorize } from './MPTokenAuthorize'
import {
MPTokenIssuanceCreate,
validateMPTokenIssuanceCreate,
} from './MPTokenIssuanceCreate'
import {
MPTokenIssuanceDestroy,
validateMPTokenIssuanceDestroy,
} from './MPTokenIssuanceDestroy'
import {
MPTokenIssuanceSet,
validateMPTokenIssuanceSet,
} from './MPTokenIssuanceSet'
import {
NFTokenAcceptOffer,
validateNFTokenAcceptOffer,
@@ -109,12 +125,19 @@ export type SubmittableTransaction =
| CheckCash
| CheckCreate
| Clawback
| CredentialAccept
| CredentialCreate
| CredentialDelete
| DIDDelete
| DIDSet
| DepositPreauth
| EscrowCancel
| EscrowCreate
| EscrowFinish
| MPTokenAuthorize
| MPTokenIssuanceCreate
| MPTokenIssuanceDestroy
| MPTokenIssuanceSet
| NFTokenAcceptOffer
| NFTokenBurn
| NFTokenCancelOffer
@@ -282,6 +305,18 @@ export function validate(transaction: Record<string, unknown>): void {
validateClawback(tx)
break
case 'CredentialAccept':
validateCredentialAccept(tx)
break
case 'CredentialCreate':
validateCredentialCreate(tx)
break
case 'CredentialDelete':
validateCredentialDelete(tx)
break
case 'DIDDelete':
validateDIDDelete(tx)
break
@@ -306,6 +341,22 @@ export function validate(transaction: Record<string, unknown>): void {
validateEscrowFinish(tx)
break
case 'MPTokenAuthorize':
validateMPTokenAuthorize(tx)
break
case 'MPTokenIssuanceCreate':
validateMPTokenIssuanceCreate(tx)
break
case 'MPTokenIssuanceDestroy':
validateMPTokenIssuanceDestroy(tx)
break
case 'MPTokenIssuanceSet':
validateMPTokenIssuanceSet(tx)
break
case 'NFTokenAcceptOffer':
validateNFTokenAcceptOffer(tx)
break

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-param-reassign -- param reassign is safe */
/* eslint-disable no-bitwise -- flags require bitwise operations */
import { ValidationError } from '../../errors'
import {
AccountRootFlagsInterface,
@@ -10,6 +9,9 @@ import { AccountSetTfFlags } from '../transactions/accountSet'
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'
import { NFTokenCreateOfferFlags } from '../transactions/NFTokenCreateOffer'
import { NFTokenMintFlags } from '../transactions/NFTokenMint'
import { OfferCreateFlags } from '../transactions/offerCreate'
@@ -49,6 +51,9 @@ const txToFlag = {
AccountSet: AccountSetTfFlags,
AMMDeposit: AMMDepositFlags,
AMMWithdraw: AMMWithdrawFlags,
MPTokenAuthorize: MPTokenAuthorizeFlags,
MPTokenIssuanceCreate: MPTokenIssuanceCreateFlags,
MPTokenIssuanceSet: MPTokenIssuanceSetFlags,
NFTokenCreateOffer: NFTokenCreateOfferFlags,
NFTokenMint: NFTokenMintFlags,
OfferCreate: OfferCreateFlags,
@@ -90,3 +95,30 @@ function convertFlagsToNumber(flags: GlobalFlags, flagEnum: any): number {
return flags[flag] ? resultFlags | flagEnum[flag] : resultFlags
}, 0)
}
/**
* Convert a Transaction flags property into a map for easy interpretation.
*
* @param tx - A transaction to parse flags for.
* @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) {
return {}
}
const flags = tx.Flags
const flagsMap = {}
// 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
}
})
return flagsMap
}

View File

@@ -1,4 +1,5 @@
const HEX_REGEX = /^[0-9A-Fa-f]+$/u
export const INTEGER_SANITY_CHECK = /^[0-9]+$/u
/**
* Verify that all fields of an object are in fields.

View File

@@ -5,6 +5,7 @@
"close_flags": 0,
"ledger_index": "15202439",
"close_time_human": "2015-Aug-12 01:01:10.000000000 UTC",
"close_time_iso": "2015-08-12T01:01.10Z",
"close_time_resolution": 10,
"closed": true,
"hash": "F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349",

View File

@@ -1,7 +1,7 @@
To run integration tests:
1. Run rippled in standalone node, either in a docker container (preferred) or by installing rippled.
* Go to the top-level of the `xrpl.js` repo, just above the `packages` folder.
* With docker, run `docker run -p 6006:6006 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.2.0-b3 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg`
* With docker, run `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'`
* Or [download and build rippled](https://xrpl.org/install-rippled.html) and run `./rippled -a --start`
* If you'd like to use the latest rippled amendments, you should modify your `rippled.cfg` file to enable amendments in the `[amendments]` section. You can view `.ci-config/rippled.cfg` in the top level folder as an example of this.
2. Run `npm run test:integration` or `npm run test:browser`

View File

@@ -6,6 +6,11 @@ import {
TrustSet,
Payment,
Clawback,
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlags,
MPTokenAuthorize,
TransactionMetadata,
LedgerEntryResponse,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
@@ -112,4 +117,92 @@ describe('Clawback', function () {
},
TIMEOUT,
)
it(
'MPToken',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTCanClawback,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
const holderAuthTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, holderAuthTx, wallet2)
const paymentTx: Payment = {
TransactionType: 'Payment',
Account: testContext.wallet.classicAddress,
Amount: { mpt_issuance_id: mptID!, value: '9223372036854775807' },
Destination: wallet2.classicAddress,
}
await testTransaction(testContext.client, paymentTx, testContext.wallet)
let ledgerEntryResponse: LedgerEntryResponse =
await testContext.client.request({
command: 'ledger_entry',
mptoken: {
mpt_issuance_id: mptID!,
account: wallet2.classicAddress,
},
})
assert.equal(
// @ts-expect-error: Known issue with unknown object type
ledgerEntryResponse.result.node.MPTAmount,
'9223372036854775807',
)
// actual test - clawback
const clawTx: Clawback = {
TransactionType: 'Clawback',
Account: testContext.wallet.classicAddress,
Amount: {
mpt_issuance_id: mptID!,
value: '500',
},
Holder: wallet2.classicAddress,
}
await testTransaction(testContext.client, clawTx, testContext.wallet)
ledgerEntryResponse = await testContext.client.request({
command: 'ledger_entry',
mptoken: {
mpt_issuance_id: mptID!,
account: wallet2.classicAddress,
},
})
assert.equal(
// @ts-expect-error: Known issue with unknown object type
ledgerEntryResponse.result.node.MPTAmount,
'9223372036854775307',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,62 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import {
AccountObjectsResponse,
CredentialAccept,
CredentialCreate,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { generateFundedWallet, testTransaction } from '../utils'
describe('CredentialAccept', function () {
// testContext wallet acts as issuer in this test
let testContext: XrplIntegrationTestContext
beforeAll(async () => {
testContext = await setupClient(serverUrl)
})
afterAll(async () => teardownClient(testContext))
it('base', async function () {
const subjectWallet = await generateFundedWallet(testContext.client)
const credentialCreateTx: CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: testContext.wallet.classicAddress,
Subject: subjectWallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialCreateTx,
testContext.wallet,
)
const credentialAcceptTx: CredentialAccept = {
TransactionType: 'CredentialAccept',
Account: subjectWallet.classicAddress,
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(testContext.client, credentialAcceptTx, subjectWallet)
// Credential is now an object in recipient's wallet after accept
const accountObjectsResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: subjectWallet.classicAddress,
type: 'credential',
})
const { account_objects } = accountObjectsResponse.result
assert.equal(account_objects.length, 1)
})
})

View File

@@ -0,0 +1,49 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import { AccountObjectsResponse, CredentialCreate } from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { generateFundedWallet, testTransaction } from '../utils'
describe('CredentialCreate', function () {
// testContext wallet acts as issuer in this test
let testContext: XrplIntegrationTestContext
beforeAll(async () => {
testContext = await setupClient(serverUrl)
})
afterAll(async () => teardownClient(testContext))
it('base', async function () {
const subjectWallet = await generateFundedWallet(testContext.client)
const credentialCreateTx: CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: testContext.wallet.classicAddress,
Subject: subjectWallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialCreateTx,
testContext.wallet,
)
// Unaccepted credential still belongs to issuer's account
const accountObjectsResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'credential',
})
const { account_objects } = accountObjectsResponse.result
assert.equal(account_objects.length, 1)
})
})

View File

@@ -0,0 +1,105 @@
import { stringToHex } from '@xrplf/isomorphic/utils'
import { assert } from 'chai'
import {
AccountObjectsResponse,
CredentialAccept,
CredentialCreate,
} from '../../../src'
import { CredentialDelete } from '../../../src/models/transactions/CredentialDelete'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { generateFundedWallet, testTransaction } from '../utils'
describe('CredentialDelete', function () {
// testContext wallet acts as issuer in this test
let testContext: XrplIntegrationTestContext
beforeAll(async () => {
testContext = await setupClient(serverUrl)
})
afterAll(async () => teardownClient(testContext))
it('base', async function () {
const subjectWallet = await generateFundedWallet(testContext.client)
const credentialCreateTx: CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: testContext.wallet.classicAddress,
Subject: subjectWallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialCreateTx,
testContext.wallet,
)
const createAccountObjectsResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'credential',
})
assert.equal(createAccountObjectsResponse.result.account_objects.length, 1)
const credentialAcceptTx: CredentialAccept = {
TransactionType: 'CredentialAccept',
Account: subjectWallet.classicAddress,
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(testContext.client, credentialAcceptTx, subjectWallet)
// Credential is now an object in recipient's wallet after accept
const acceptAccountObjectsResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: subjectWallet.classicAddress,
type: 'credential',
})
assert.equal(acceptAccountObjectsResponse.result.account_objects.length, 1)
const credentialDeleteTx: CredentialDelete = {
TransactionType: 'CredentialDelete',
Account: subjectWallet.classicAddress,
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(testContext.client, credentialDeleteTx, subjectWallet)
// Check both issuer and subject no longer have a credential tied to the account
const SubjectAccountObjectsDeleteResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: subjectWallet.classicAddress,
type: 'credential',
})
assert.equal(
SubjectAccountObjectsDeleteResponse.result.account_objects.length,
0,
)
const IssuerAccountObjectsDeleteResponse: AccountObjectsResponse =
await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'credential',
})
assert.equal(
IssuerAccountObjectsDeleteResponse.result.account_objects.length,
0,
)
})
})

View File

@@ -1,11 +1,19 @@
import { DepositPreauth, Wallet } from '../../../src'
import { stringToHex } from '@xrplf/isomorphic/utils'
import {
AuthorizeCredential,
CredentialAccept,
CredentialCreate,
DepositPreauth,
Wallet,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { fundAccount, testTransaction } from '../utils'
import { fundAccount, generateFundedWallet, testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
@@ -32,4 +40,119 @@ describe('DepositPreauth', function () {
},
TIMEOUT,
)
it(
'AuthorizeCredential base case',
async () => {
const subjectWallet = await generateFundedWallet(testContext.client)
const credentialCreateTx: CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: testContext.wallet.classicAddress,
Subject: subjectWallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialCreateTx,
testContext.wallet,
)
const credentialAcceptTx: CredentialAccept = {
TransactionType: 'CredentialAccept',
Account: subjectWallet.classicAddress,
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialAcceptTx,
subjectWallet,
)
const authorizeCredentialObj: AuthorizeCredential = {
Credential: {
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
},
}
const wallet2 = Wallet.generate()
await fundAccount(testContext.client, wallet2)
const tx: DepositPreauth = {
TransactionType: 'DepositPreauth',
Account: testContext.wallet.classicAddress,
AuthorizeCredentials: [authorizeCredentialObj],
}
await testTransaction(testContext.client, tx, testContext.wallet)
},
TIMEOUT,
)
it(
'UnauthorizeCredential base case',
async () => {
const subjectWallet = await generateFundedWallet(testContext.client)
const credentialCreateTx: CredentialCreate = {
TransactionType: 'CredentialCreate',
Account: testContext.wallet.classicAddress,
Subject: subjectWallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialCreateTx,
testContext.wallet,
)
const credentialAcceptTx: CredentialAccept = {
TransactionType: 'CredentialAccept',
Account: subjectWallet.classicAddress,
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
}
await testTransaction(
testContext.client,
credentialAcceptTx,
subjectWallet,
)
const authorizeCredentialObj: AuthorizeCredential = {
Credential: {
Issuer: testContext.wallet.classicAddress,
CredentialType: stringToHex('Test Credential Type'),
},
}
const wallet2 = Wallet.generate()
await fundAccount(testContext.client, wallet2)
const authCredDepositPreauthTx: DepositPreauth = {
TransactionType: 'DepositPreauth',
Account: testContext.wallet.classicAddress,
AuthorizeCredentials: [authorizeCredentialObj],
}
await testTransaction(
testContext.client,
authCredDepositPreauthTx,
testContext.wallet,
)
const UnauthCredDepositPreauthTx: DepositPreauth = {
TransactionType: 'DepositPreauth',
Account: testContext.wallet.classicAddress,
UnauthorizeCredentials: [authorizeCredentialObj],
}
await testTransaction(
testContext.client,
UnauthCredDepositPreauthTx,
testContext.wallet,
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,119 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenAuthorize,
MPTokenIssuanceCreateFlags,
MPTokenAuthorizeFlags,
TransactionMetadata,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction, generateFundedWallet } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('MPTokenAuthorize', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTRequireAuth,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
let authTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'mptoken',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Holder owns 1 MPToken on the ledger',
)
authTx = {
TransactionType: 'MPTokenAuthorize',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
Holder: wallet2.classicAddress,
}
await testTransaction(testContext.client, authTx, testContext.wallet)
authTx = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
0,
'Holder owns nothing on the ledger',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,54 @@
import { assert } from 'chai'
import { MPTokenIssuanceCreate } from '../../../src'
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('MPTokenIssuanceCreate', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const tx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
// 0x7fffffffffffffff
MaximumAmount: '9223372036854775807',
AssetScale: 2,
}
await testTransaction(testContext.client, tx, testContext.wallet)
const accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
assert.equal(
// @ts-expect-error: Known issue with unknown object type
accountObjectsResponse.result.account_objects[0].MaximumAmount,
`9223372036854775807`,
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceDestroy,
TransactionMetadata,
} from '../../../src'
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('MPTokenIssuanceDestroy', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const destroyTx: MPTokenIssuanceDestroy = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, destroyTx, testContext.wallet)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
0,
'Should be zero issuance on the ledger',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,78 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceSet,
MPTokenIssuanceCreateFlags,
MPTokenIssuanceSetFlags,
TransactionMetadata,
} from '../../../src'
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('MPTokenIssuanceDestroy', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
const accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const setTx: MPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
}
await testTransaction(testContext.client, setTx, testContext.wallet)
},
TIMEOUT,
)
})

View File

@@ -1,6 +1,12 @@
import { assert } from 'chai'
import { Payment, Wallet } from '../../../src'
import {
Payment,
Wallet,
MPTokenIssuanceCreate,
MPTokenAuthorize,
TransactionMetadata,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
@@ -102,4 +108,89 @@ describe('Payment', function () {
},
TIMEOUT,
)
it(
'Validate MPT Payment ',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id!
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const authTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'mptoken',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Holder owns 1 MPToken on the ledger',
)
const payTx: Payment = {
TransactionType: 'Payment',
Account: testContext.wallet.classicAddress,
Destination: wallet2.classicAddress,
Amount: {
mpt_issuance_id: mptID,
value: '100',
},
}
await testTransaction(testContext.client, payTx, testContext.wallet)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.equal(
// @ts-expect-error -- Object type not known
accountObjectsResponse.result.account_objects[0].OutstandingAmount,
`100`,
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,153 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateCredentialAccept } from '../../src/models/transactions/CredentialAccept'
/**
* CredentialAccept Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('CredentialAccept', function () {
let credentialAccept
beforeEach(function () {
credentialAccept = {
TransactionType: 'CredentialAccept',
Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Account: 'rNdY9XDnQ4Dr1EgefwU3CBRuAjt3sAutGg',
CredentialType: stringToHex('Passport'),
Sequence: 1337,
Flags: 0,
} as any
})
it(`verifies valid CredentialAccept`, function () {
assert.doesNotThrow(() => validateCredentialAccept(credentialAccept))
assert.doesNotThrow(() => validate(credentialAccept))
})
it(`throws w/ missing field Account`, function () {
credentialAccept.Account = undefined
const errorMessage = 'CredentialAccept: missing field Account'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ Account not a string`, function () {
credentialAccept.Account = 123
const errorMessage = 'CredentialAccept: invalid field Account'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field Issuer`, function () {
credentialAccept.Issuer = undefined
const errorMessage = 'CredentialAccept: missing field Issuer'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ Issuer not a string`, function () {
credentialAccept.Issuer = 123
const errorMessage = 'CredentialAccept: invalid field Issuer'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field CredentialType`, function () {
credentialAccept.CredentialType = undefined
const errorMessage = 'CredentialAccept: missing field CredentialType'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field too long`, function () {
credentialAccept.CredentialType = stringToHex('A'.repeat(129))
const errorMessage =
'CredentialAccept: CredentialType length cannot be > 128'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field empty`, function () {
credentialAccept.CredentialType = ''
const errorMessage =
'CredentialAccept: CredentialType cannot be an empty string'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field not hex`, function () {
credentialAccept.CredentialType = 'this is not hex'
const errorMessage =
'CredentialAccept: CredentialType must be encoded in hex'
assert.throws(
() => validateCredentialAccept(credentialAccept),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialAccept),
ValidationError,
errorMessage,
)
})
})

View File

@@ -0,0 +1,230 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateCredentialCreate } from '../../src/models/transactions/CredentialCreate'
/**
* CredentialCreate Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('credentialCreate', function () {
let credentialCreate
beforeEach(function () {
credentialCreate = {
TransactionType: 'CredentialCreate',
Account: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Subject: 'rNdY9XDnQ4Dr1EgefwU3CBRuAjt3sAutGg',
CredentialType: stringToHex('Passport'),
Expiration: 1212025,
URI: stringToHex('TestURI'),
Sequence: 1337,
Flags: 0,
} as any
})
it(`verifies valid credentialCreate`, function () {
assert.doesNotThrow(() => validateCredentialCreate(credentialCreate))
assert.doesNotThrow(() => validate(credentialCreate))
})
it(`throws w/ missing field Account`, function () {
credentialCreate.Account = undefined
const errorMessage = 'CredentialCreate: missing field Account'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ Account not string`, function () {
credentialCreate.Account = 123
const errorMessage = 'CredentialCreate: invalid field Account'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field Subject`, function () {
credentialCreate.Subject = undefined
const errorMessage = 'CredentialCreate: missing field Subject'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ Subject not string`, function () {
credentialCreate.Subject = 123
const errorMessage = 'CredentialCreate: invalid field Subject'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field credentialType`, function () {
credentialCreate.CredentialType = undefined
const errorMessage = 'CredentialCreate: missing field CredentialType'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field too long`, function () {
credentialCreate.CredentialType = stringToHex('A'.repeat(129))
const errorMessage =
'CredentialCreate: CredentialType length cannot be > 128'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field empty`, function () {
credentialCreate.CredentialType = ''
const errorMessage =
'CredentialCreate: CredentialType cannot be an empty string'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field not hex`, function () {
credentialCreate.CredentialType = 'this is not hex'
const errorMessage =
'CredentialCreate: CredentialType must be encoded in hex'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ Expiration field not number`, function () {
credentialCreate.Expiration = 'this is not a number'
const errorMessage = 'CredentialCreate: invalid field Expiration'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ URI field not a string`, function () {
credentialCreate.URI = 123
const errorMessage = 'CredentialCreate: invalid field URI'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ URI field empty`, function () {
credentialCreate.URI = ''
const errorMessage = 'CredentialCreate: URI cannot be an empty string'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ URI field too long`, function () {
credentialCreate.URI = stringToHex('A'.repeat(129))
const errorMessage = 'CredentialCreate: URI length must be <= 256'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
it(`throws w/ URI field not hex`, function () {
credentialCreate.URI = 'this is not hex'
const errorMessage = 'CredentialCreate: URI must be encoded in hex'
assert.throws(
() => validateCredentialCreate(credentialCreate),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialCreate),
ValidationError,
errorMessage,
)
})
})

View File

@@ -0,0 +1,171 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateCredentialDelete } from '../../src/models/transactions/CredentialDelete'
/**
* CredentialDelete Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('CredentialDelete', function () {
let credentialDelete
beforeEach(function () {
credentialDelete = {
TransactionType: 'CredentialDelete',
Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ',
Subject: 'rNdY9XDnQ4Dr1EgefwU3CBRuAjt3sAutGg',
Account: 'rNdY9XDnQ4Dr1EgefwU3CBRuAjt3sAutGg',
CredentialType: stringToHex('Passport'),
Sequence: 1337,
Flags: 0,
} as any
})
it(`verifies valid credentialDelete`, function () {
assert.doesNotThrow(() => validateCredentialDelete(credentialDelete))
assert.doesNotThrow(() => validate(credentialDelete))
})
it(`throws w/ missing field Account`, function () {
credentialDelete.Account = undefined
const errorMessage = 'CredentialDelete: missing field Account'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ Account not string`, function () {
credentialDelete.Account = 123
const errorMessage = 'CredentialDelete: invalid field Account'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ Subject not string`, function () {
credentialDelete.Subject = 123
const errorMessage = 'CredentialDelete: invalid field Subject'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ Issuer not string`, function () {
credentialDelete.Issuer = 123
const errorMessage = 'CredentialDelete: invalid field Issuer'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field Subject and Issuer`, function () {
credentialDelete.Subject = undefined
credentialDelete.Issuer = undefined
const errorMessage =
'CredentialDelete: either `Issuer` or `Subject` must be provided'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ missing field credentialType`, function () {
credentialDelete.CredentialType = undefined
const errorMessage = 'CredentialDelete: missing field CredentialType'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field too long`, function () {
credentialDelete.CredentialType = stringToHex('A'.repeat(129))
const errorMessage =
'CredentialDelete: CredentialType length cannot be > 128'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field empty`, function () {
credentialDelete.CredentialType = ''
const errorMessage =
'CredentialDelete: CredentialType cannot be an empty string'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ credentialType field not hex`, function () {
credentialDelete.CredentialType = 'this is not hex'
const errorMessage =
'CredentialDelete: CredentialType must be encoded in hex'
assert.throws(
() => validateCredentialDelete(credentialDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(credentialDelete),
ValidationError,
errorMessage,
)
})
})

View File

@@ -0,0 +1,63 @@
import { assert } from 'chai'
import { validate, ValidationError, MPTokenAuthorizeFlags } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenAuthorize Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenAuthorize', function () {
it(`verifies valid MPTokenAuthorize`, function () {
let validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenAuthorize: missing field MPTokenIssuanceID',
)
})
})

View File

@@ -0,0 +1,149 @@
import { assert } from 'chai'
import {
convertStringToHex,
validate,
ValidationError,
MPTokenIssuanceCreateFlags,
} from '../../src'
/**
* MPTokenIssuanceCreate Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceCreate', function () {
it(`verifies valid MPTokenIssuanceCreate`, function () {
const validMPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
// 0x7fffffffffffffff
MaximumAmount: '9223372036854775807',
AssetScale: 2,
TransferFee: 1,
Flags: MPTokenIssuanceCreateFlags.tfMPTCanTransfer,
MPTokenMetadata: convertStringToHex('http://xrpl.org'),
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceCreate))
})
it(`throws w/ MPTokenMetadata being an empty string`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
MPTokenMetadata: '',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MPTokenMetadata must not be empty string',
)
})
it(`throws w/ MPTokenMetadata not in hex format`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
MPTokenMetadata: 'http://xrpl.org',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MPTokenMetadata must be in hex format',
)
})
it(`throws w/ Invalid MaximumAmount`, function () {
let invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '9223372036854775808',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MaximumAmount out of range',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '-1',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: Invalid MaximumAmount',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '0x12',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: Invalid MaximumAmount',
)
})
it(`throws w/ Invalid TransferFee`, function () {
let invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: -1,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee must be between 0 and 50000',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: 50001,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee must be between 0 and 50000',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: 100,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee cannot be provided without enabling tfMPTCanTransfer flag',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: 100,
Flags: { tfMPTCanClawback: true },
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee cannot be provided without enabling tfMPTCanTransfer flag',
)
})
})

View File

@@ -0,0 +1,35 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenIssuanceDestroy Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceDestroy', function () {
it(`verifies valid MPTokenIssuanceDestroy`, function () {
const validMPTokenIssuanceDestroy = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceDestroy))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceDestroy: missing field MPTokenIssuanceID',
)
})
})

View File

@@ -0,0 +1,82 @@
import { assert } from 'chai'
import { validate, ValidationError, MPTokenIssuanceSetFlags } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenIssuanceSet Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceSet', function () {
it(`verifies valid MPTokenIssuanceSet`, function () {
let validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
// It's fine to not specify any flag, it means only tx fee is deducted
validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceSet: missing field MPTokenIssuanceID',
)
})
it(`throws w/ conflicting flags`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
} as any
invalid.Flags =
// eslint-disable-next-line no-bitwise -- not needed
MPTokenIssuanceSetFlags.tfMPTLock | MPTokenIssuanceSetFlags.tfMPTUnlock
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceSet: flag conflict',
)
invalid.Flags = { tfMPTLock: true, tfMPTUnlock: true }
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceSet: flag conflict',
)
})
})

View File

@@ -9,8 +9,10 @@ import { validateAccountDelete } from '../../src/models/transactions/accountDele
* Providing runtime verification testing for each specific transaction type.
*/
describe('AccountDelete', function () {
it(`verifies valid AccountDelete`, function () {
const validAccountDelete = {
let validAccountDelete
beforeEach(() => {
validAccountDelete = {
TransactionType: 'AccountDelete',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
@@ -18,76 +20,166 @@ describe('AccountDelete', function () {
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
CredentialIDs: [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
],
} as any
})
it(`verifies valid AccountDelete`, function () {
assert.doesNotThrow(() => validateAccountDelete(validAccountDelete))
})
it(`throws w/ missing Destination`, function () {
const invalidDestination = {
TransactionType: 'AccountDelete',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
validAccountDelete.Destination = undefined
const errorMessage = 'AccountDelete: missing field Destination'
assert.throws(
() => validateAccountDelete(invalidDestination),
() => validateAccountDelete(validAccountDelete),
ValidationError,
'AccountDelete: missing field Destination',
errorMessage,
)
assert.throws(
() => validate(invalidDestination),
() => validate(validAccountDelete),
ValidationError,
'AccountDelete: missing field Destination',
errorMessage,
)
})
it(`throws w/ invalid Destination`, function () {
const invalidDestination = {
TransactionType: 'AccountDelete',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Destination: 65478965,
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
validAccountDelete.Destination = 65478965
const errorMessage = 'AccountDelete: invalid field Destination'
assert.throws(
() => validateAccountDelete(invalidDestination),
() => validateAccountDelete(validAccountDelete),
ValidationError,
'AccountDelete: invalid field Destination',
errorMessage,
)
assert.throws(
() => validate(invalidDestination),
() => validate(validAccountDelete),
ValidationError,
'AccountDelete: invalid field Destination',
errorMessage,
)
})
it(`throws w/ invalid DestinationTag`, function () {
const invalidDestinationTag = {
TransactionType: 'AccountDelete',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
DestinationTag: 'gvftyujnbv',
Fee: '5000000',
Sequence: 2470665,
Flags: 2147483648,
} as any
validAccountDelete.DestinationTag = 'gvftyujnbv'
const errorMessage = 'AccountDelete: invalid field DestinationTag'
assert.throws(
() => validateAccountDelete(invalidDestinationTag),
() => validateAccountDelete(validAccountDelete),
ValidationError,
'AccountDelete: invalid field DestinationTag',
errorMessage,
)
assert.throws(
() => validate(invalidDestinationTag),
() => validate(validAccountDelete),
ValidationError,
'AccountDelete: invalid field DestinationTag',
errorMessage,
)
})
it(`throws w/ non-array CredentialIDs`, function () {
validAccountDelete.CredentialIDs =
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A'
const errorMessage = 'AccountDelete: Credentials must be an array'
assert.throws(
() => validateAccountDelete(validAccountDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(validAccountDelete),
ValidationError,
errorMessage,
)
})
it(`throws CredentialIDs length exceeds max length`, function () {
validAccountDelete.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66B',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66C',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66D',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66E',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66F',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F660',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F661',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage =
'AccountDelete: Credentials length cannot exceed 8 elements'
assert.throws(
() => validateAccountDelete(validAccountDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(validAccountDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ empty CredentialIDs`, function () {
validAccountDelete.CredentialIDs = []
const errorMessage = 'AccountDelete: Credentials cannot be an empty array'
assert.throws(
() => validateAccountDelete(validAccountDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(validAccountDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ non-string CredentialIDs`, function () {
validAccountDelete.CredentialIDs = [
123123,
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage = 'AccountDelete: Invalid Credentials ID list format'
assert.throws(
() => validateAccountDelete(validAccountDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(validAccountDelete),
ValidationError,
errorMessage,
)
})
it(`throws w/ duplicate CredentialIDs`, function () {
validAccountDelete.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage =
'AccountDelete: Credentials cannot contain duplicate elements'
assert.throws(
() => validateAccountDelete(validAccountDelete),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(validAccountDelete),
ValidationError,
errorMessage,
)
})
})

View File

@@ -78,4 +78,72 @@ describe('Clawback', function () {
'Clawback: invalid holder Account',
)
})
it(`verifies valid MPT Clawback`, function () {
const validClawback = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.doesNotThrow(() => validate(validClawback))
})
it(`throws w/ invalid Holder Account`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: invalid holder Account',
)
})
it(`throws w/ invalid Holder`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: missing Holder',
)
})
it(`throws w/ invalid currency Holder`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
currency: 'DSH',
issuer: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
value: '43.11584856965009',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: cannot have Holder for currency',
)
})
})

View File

@@ -1,6 +1,7 @@
import { stringToHex } from '@xrplf/isomorphic/dist/utils'
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { AuthorizeCredential, validate, ValidationError } from '../../src'
import { validateDepositPreauth } from '../../src/models/transactions/depositPreauth'
/**
@@ -11,6 +12,13 @@ import { validateDepositPreauth } from '../../src/models/transactions/depositPre
describe('DepositPreauth', function () {
let depositPreauth
const validCredential = {
Credential: {
Issuer: 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW',
CredentialType: stringToHex('Passport'),
},
}
beforeEach(function () {
depositPreauth = {
TransactionType: 'DepositPreauth',
@@ -30,32 +38,73 @@ describe('DepositPreauth', function () {
assert.doesNotThrow(() => validate(depositPreauth))
})
it('throws when both Authorize and Unauthorize are provided', function () {
it('verifies valid DepositPreauth when only AuthorizeCredentials is provided', function () {
depositPreauth.AuthorizeCredentials = [validCredential]
assert.doesNotThrow(() => validateDepositPreauth(depositPreauth))
assert.doesNotThrow(() => validate(depositPreauth))
})
it('verifies valid DepositPreauth when only UnauthorizeCredentials is provided', function () {
depositPreauth.UnauthorizeCredentials = [validCredential]
assert.doesNotThrow(() => validateDepositPreauth(depositPreauth))
assert.doesNotThrow(() => validate(depositPreauth))
})
it('throws when multiple of Authorize, Unauthorize, AuthorizeCredentials, UnauthorizeCredentials are provided', function () {
const errorMessage =
'DepositPreauth: Requires exactly one field of the following: Authorize, Unauthorize, AuthorizeCredentials, UnauthorizeCredentials.'
depositPreauth.Authorize = 'rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW'
depositPreauth.UnauthorizeCredentials = [validCredential]
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
depositPreauth.Unauthorize = 'raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n'
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
"DepositPreauth: can't provide both Authorize and Unauthorize fields",
errorMessage,
)
assert.throws(
() => validate(depositPreauth),
ValidationError,
"DepositPreauth: can't provide both Authorize and Unauthorize fields",
)
})
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
it('throws when neither Authorize nor Unauthorize are provided', function () {
depositPreauth.AuthorizeCredentials = [validCredential]
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
'DepositPreauth: must provide either Authorize or Unauthorize field',
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
depositPreauth.Authorize = undefined
assert.throws(
() => validate(depositPreauth),
() => validateDepositPreauth(depositPreauth),
ValidationError,
'DepositPreauth: must provide either Authorize or Unauthorize field',
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
depositPreauth.UnauthorizeCredentials = undefined
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when none of Authorize, Unauthorize, AuthorizeCredentials, UnauthorizeCredentials are provided', function () {
const errorMessage =
'DepositPreauth: Requires exactly one field of the following: Authorize, Unauthorize, AuthorizeCredentials, UnauthorizeCredentials.'
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when Authorize is not a string', function () {
@@ -108,4 +157,154 @@ describe('DepositPreauth', function () {
"DepositPreauth: Account can't unauthorize its own address",
)
})
it('throws when AuthorizeCredentials is not an array', function () {
const errorMessage = 'DepositPreauth: Credentials must be an array'
depositPreauth.AuthorizeCredentials = validCredential
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when UnauthorizeCredentials is not an array', function () {
const errorMessage = 'DepositPreauth: Credentials must be an array'
depositPreauth.UnauthorizeCredentials = validCredential
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when AuthorizeCredentials is empty array', function () {
const errorMessage = 'DepositPreauth: Credentials cannot be an empty array'
depositPreauth.AuthorizeCredentials = []
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when UnauthorizeCredentials is empty array', function () {
const errorMessage = 'DepositPreauth: Credentials cannot be an empty array'
depositPreauth.UnauthorizeCredentials = []
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when AuthorizeCredentials is too long', function () {
const sampleCredentials: AuthorizeCredential[] = []
const errorMessage =
'DepositPreauth: Credentials length cannot exceed 8 elements'
for (let index = 0; index < 9; index++) {
sampleCredentials.push({
Credential: {
Issuer: `SampleIssuer${index}`,
CredentialType: stringToHex('Passport'),
},
})
}
depositPreauth.AuthorizeCredentials = sampleCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when UnauthorizeCredentials is too long', function () {
const sampleCredentials: AuthorizeCredential[] = []
const errorMessage =
'DepositPreauth: Credentials length cannot exceed 8 elements'
for (let index = 0; index < 9; index++) {
sampleCredentials.push({
Credential: {
Issuer: `SampleIssuer${index}`,
CredentialType: stringToHex('Passport'),
},
})
}
depositPreauth.UnauthorizeCredentials = sampleCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when AuthorizeCredentials is invalid shape', function () {
const invalidCredentials = [
{ Credential: 'Invalid Shape' },
{ Credential: 'Another Invalid Shape' },
]
const errorMessage = 'DepositPreauth: Invalid Credentials format'
depositPreauth.AuthorizeCredentials = invalidCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when UnauthorizeCredentials is invalid shape', function () {
const invalidCredentials = [
{ Credential: 'Invalid Shape' },
{ Credential: 'Another Invalid Shape' },
]
const errorMessage = 'DepositPreauth: Invalid Credentials format'
depositPreauth.UnauthorizeCredentials = invalidCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when AuthorizeCredentials has duplicates', function () {
const invalidCredentials = [validCredential, validCredential]
const errorMessage =
'DepositPreauth: Credentials cannot contain duplicate elements'
depositPreauth.AuthorizeCredentials = invalidCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
it('throws when UnauthorizeCredentials has duplicates', function () {
const invalidCredentials = [validCredential, validCredential]
const errorMessage =
'DepositPreauth: Credentials cannot contain duplicate elements'
depositPreauth.UnauthorizeCredentials = invalidCredentials
assert.throws(
() => validateDepositPreauth(depositPreauth),
ValidationError,
errorMessage,
)
assert.throws(() => validate(depositPreauth), ValidationError, errorMessage)
})
})

View File

@@ -20,6 +20,9 @@ describe('EscrowFinish', function () {
Condition:
'A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100',
Fulfillment: 'A0028000',
CredentialIDs: [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
],
}
})
it(`verifies valid EscrowFinish`, function () {
@@ -28,8 +31,9 @@ describe('EscrowFinish', function () {
})
it(`verifies valid EscrowFinish w/o optional`, function () {
delete escrow.Condition
delete escrow.Fulfillment
escrow.Condition = undefined
escrow.Fulfillment = undefined
escrow.CredentialIDs = undefined
assert.doesNotThrow(() => validateEscrowFinish(escrow))
assert.doesNotThrow(() => validate(escrow))
@@ -101,4 +105,88 @@ describe('EscrowFinish', function () {
'EscrowFinish: Fulfillment must be a string',
)
})
it(`throws w/ non-array CredentialIDs`, function () {
escrow.CredentialIDs =
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A'
const errorMessage = 'EscrowFinish: Credentials must be an array'
assert.throws(
() => validateEscrowFinish(escrow),
ValidationError,
errorMessage,
)
assert.throws(() => validate(escrow), ValidationError, errorMessage)
})
it(`throws CredentialIDs length exceeds max length`, function () {
escrow.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66B',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66C',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66D',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66E',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66F',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F660',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F661',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage =
'EscrowFinish: Credentials length cannot exceed 8 elements'
assert.throws(
() => validateEscrowFinish(escrow),
ValidationError,
errorMessage,
)
assert.throws(() => validate(escrow), ValidationError, errorMessage)
})
it(`throws w/ empty CredentialIDs`, function () {
escrow.CredentialIDs = []
const errorMessage = 'EscrowFinish: Credentials cannot be an empty array'
assert.throws(
() => validateEscrowFinish(escrow),
ValidationError,
errorMessage,
)
assert.throws(() => validate(escrow), ValidationError, errorMessage)
})
it(`throws w/ non-string CredentialIDs`, function () {
escrow.CredentialIDs = [
123123,
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage = 'EscrowFinish: Invalid Credentials ID list format'
assert.throws(
() => validateEscrowFinish(escrow),
ValidationError,
errorMessage,
)
assert.throws(() => validate(escrow), ValidationError, errorMessage)
})
it(`throws w/ duplicate CredentialIDs`, function () {
escrow.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage =
'EscrowFinish: Credentials cannot contain duplicate elements'
assert.throws(
() => validateEscrowFinish(escrow),
ValidationError,
errorMessage,
)
assert.throws(() => validate(escrow), ValidationError, errorMessage)
})
})

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-statements -- need additional tests for optional fields */
import { assert } from 'chai'
import { validate, PaymentFlags, ValidationError } from '../../src'
@@ -258,4 +259,121 @@ describe('Payment', function () {
'PaymentTransaction: tfPartialPayment flag required with DeliverMin',
)
})
it(`verifies valid MPT PaymentTransaction`, function () {
const mptPaymentTransaction = {
TransactionType: 'Payment',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.doesNotThrow(() => validatePayment(mptPaymentTransaction))
assert.doesNotThrow(() => validate(mptPaymentTransaction))
})
it(`throws w/ non-array CredentialIDs`, function () {
paymentTransaction.CredentialIDs =
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A'
const errorMessage = 'Payment: Credentials must be an array'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
errorMessage,
)
})
it(`throws CredentialIDs length exceeds max length`, function () {
paymentTransaction.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66B',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66C',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66D',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66E',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66F',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F660',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F661',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage = 'Payment: Credentials length cannot exceed 8 elements'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
errorMessage,
)
})
it(`throws w/ empty CredentialIDs`, function () {
paymentTransaction.CredentialIDs = []
const errorMessage = 'Payment: Credentials cannot be an empty array'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
errorMessage,
)
})
it(`throws w/ non-string CredentialIDs`, function () {
paymentTransaction.CredentialIDs = [
123123,
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage = 'Payment: Invalid Credentials ID list format'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
errorMessage,
)
})
it(`throws w/ duplicate CredentialIDs`, function () {
paymentTransaction.CredentialIDs = [
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
'EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F662',
]
const errorMessage =
'Payment: Credentials cannot contain duplicate elements'
assert.throws(
() => validatePayment(paymentTransaction),
ValidationError,
errorMessage,
)
assert.throws(
() => validate(paymentTransaction),
ValidationError,
errorMessage,
)
})
})

View File

@@ -17,6 +17,7 @@ import { isFlagEnabled } from '../../src/models/utils'
import {
setTransactionFlagsToNumber,
parseAccountRootFlags,
parseTransactionFlags,
} from '../../src/models/utils/flags'
/**
@@ -207,5 +208,60 @@ describe('Models Utils', function () {
assert.isUndefined(parsed.lsfDisallowIncomingTrustline)
assert.isUndefined(parsed.lsfAllowTrustLineClawback)
})
it('parseTransactionFlags all enabled', function () {
const tx: PaymentChannelClaim = {
Account: 'r...',
TransactionType: 'PaymentChannelClaim',
Channel:
'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198',
Flags: {
tfRenew: true,
tfClose: false,
},
}
const expected = {
tfRenew: true,
}
const flagsMap = parseTransactionFlags(tx)
assert.notStrictEqual(flagsMap, expected)
})
it('parseTransactionFlags all false', function () {
const tx: PaymentChannelClaim = {
Account: 'r...',
TransactionType: 'PaymentChannelClaim',
Channel:
'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198',
Flags: {
tfRenew: false,
tfClose: false,
},
}
const expected = {}
const flagsMap = parseTransactionFlags(tx)
assert.notStrictEqual(flagsMap, expected)
})
it('parseTransactionFlags flag is already numeric', function () {
const tx: PaymentChannelClaim = {
Account: 'r...',
TransactionType: 'PaymentChannelClaim',
Channel:
'C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198',
Flags: 65536,
}
const expected = {
tfRenew: true,
}
const flagsMap = parseTransactionFlags(tx)
assert.notStrictEqual(flagsMap, expected)
})
})
})

View File

@@ -11,7 +11,7 @@ function getTx(txName) {
.filter((tx) => tx.json.TransactionType === txName)
.map((tx) => tx.json)
if (validTxs.length == 0) {
throw new Error(`Must have ripple-binary-codec fixture for ${txName}`)
return '{ /* TODO: add sample transaction */ }'
}
const validTx = validTxs[0]
delete validTx.TxnSignature
@@ -74,7 +74,7 @@ function getInvalidValue(paramTypes) {
} else if (paramType == 'XChainBridge') {
return JSON.stringify({ XChainDoor: 'test' })
} else {
throw Error(`${paramType} not supported yet`)
return '/*TODO*/'
}
}
@@ -86,7 +86,7 @@ function getInvalidValue(paramTypes) {
} else if (JSON.stringify(simplifiedParamTypes) === '["number","string"]') {
return JSON.stringify({ currency: 'ETH' })
} else {
throw Error(`${simplifiedParamTypes} not supported yet`)
return '/*TODO*/'
}
}

View File

@@ -15,7 +15,7 @@ let jsTransactionFile
function processRippledSource(folder) {
const sfieldCpp = readFile(
path.join(folder, 'src/ripple/protocol/impl/SField.cpp'),
path.join(folder, 'src/libxrpl/protocol/SField.cpp'),
)
const sfieldHits = sfieldCpp.match(
/^ *CONSTRUCT_[^\_]+_SFIELD *\( *[^,\n]*,[ \n]*"([^\"\n ]+)"[ \n]*,[ \n]*([^, \n]+)[ \n]*,[ \n]*([0-9]+)(,.*?(notSigning))?/gm,
@@ -29,7 +29,7 @@ function processRippledSource(folder) {
}
const txFormatsCpp = readFile(
path.join(folder, 'src/ripple/protocol/impl/TxFormats.cpp'),
path.join(folder, 'src/libxrpl/protocol/TxFormats.cpp'),
)
const txFormatsHits = txFormatsCpp.match(
/^ *add\(jss::([^\"\n, ]+),[ \n]*tt[A-Z_]+,[ \n]*{[ \n]*(({sf[A-Za-z0-9]+, soe(OPTIONAL|REQUIRED|DEFAULT)},[ \n]+)*)},[ \n]*[pseudocC]+ommonFields\);/gm,
@@ -49,10 +49,10 @@ function processRippledSource(folder) {
),
)
const transactionMatch = jsTransactionFile.match(
/export type Transaction =([| \nA-Za-z]+)\nexport/,
/export type SubmittableTransaction =([| \nA-Za-z]+)\n\/\*\*/,
)[0]
const existingLibraryTxs = transactionMatch
.replace('\n\nexport', '')
.replace('\n\n/**', '')
.split('\n | ')
.filter((value) => !value.includes('export type'))
.map((value) => value.trim())
@@ -248,10 +248,11 @@ ${validationImportLine}`
)
const validateTests = createValidateTests(tx)
fs.writeFileSync(
path.join(path.dirname(__filename), `../test/models/${tx}.test.ts`),
validateTests,
)
if (validateTests !== '')
fs.writeFileSync(
path.join(path.dirname(__filename), `../test/models/${tx}.test.ts`),
validateTests,
)
updateTransactionFile(transactionMatch, tx)
@@ -259,9 +260,7 @@ ${validationImportLine}`
console.log(`Added ${tx}`)
})
console.log(
'Future steps: Adding docstrings to the models and adding integration tests',
)
// TODO: add docstrings to the models and add integration tests
}
if (require.main === module) {