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>
This commit is contained in:
achowdhry-ripple
2024-12-20 14:03:56 -05:00
committed by GitHub
parent 7bf6fecc71
commit f34d1a7a63
36 changed files with 2056 additions and 88 deletions

View File

@@ -178,8 +178,9 @@ PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
# 2.3.0-rc1 Amendments
fixAMMv1_1
# 2.3.0 Amendments
fixAMMv1_2
Credentials
NFTokenMintOffer
MPTokensV1

View File

@@ -106,7 +106,7 @@ jobs:
- name: Run docker in background
run: |
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_nfo || 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"
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@v4
@@ -162,7 +162,7 @@ jobs:
- name: Run docker in background
run: |
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_nfo || 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"
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 10
run: |

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

View File

@@ -910,6 +910,26 @@
"type": "UInt64"
}
],
[
"IssuerNode",
{
"nth": 27,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt64"
}
],
[
"SubjectNode",
{
"nth": 28,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt64"
}
],
[
"EmailHash",
{
@@ -1810,6 +1830,16 @@
"type": "Blob"
}
],
[
"CredentialType",
{
"nth": 31,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "Blob"
}
],
[
"Account",
{
@@ -1980,6 +2010,16 @@
"type": "AccountID"
}
],
[
"Subject",
{
"nth": 24,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "AccountID"
}
],
[
"TransactionMetaData",
{
@@ -2270,6 +2310,16 @@
"type": "STObject"
}
],
[
"Credential",
{
"nth": 33,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STObject"
}
],
[
"Signers",
{
@@ -2460,6 +2510,26 @@
"type": "STArray"
}
],
[
"AuthorizeCredentials",
{
"nth": 26,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STArray"
}
],
[
"UnauthorizeCredentials",
{
"nth": 27,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "STArray"
}
],
[
"CloseResolution",
{
@@ -2640,6 +2710,16 @@
"type": "Vector256"
}
],
[
"CredentialIDs",
{
"nth": 5,
"isVLEncoded": true,
"isSerialized": true,
"isSigningField": true,
"type": "Vector256"
}
],
[
"MPTokenIssuanceID",
{
@@ -2781,6 +2861,7 @@
"NegativeUNL": 78,
"Offer": 111,
"Oracle": 128,
"Credential": 129,
"PayChannel": 120,
"RippleState": 114,
"SignerList": 83,
@@ -2797,6 +2878,7 @@
"tecAMM_NOT_EMPTY": 167,
"tecARRAY_EMPTY": 190,
"tecARRAY_TOO_LARGE": 191,
"tecBAD_CREDENTIALS": 193,
"tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158,
"tecCLAIM": 100,
"tecCRYPTOCONDITION_ERROR": 146,
@@ -2982,6 +3064,9 @@
"CheckCash": 17,
"CheckCreate": 16,
"Clawback": 30,
"CredentialCreate": 58,
"CredentialAccept": 59,
"CredentialDelete": 60,
"DIDDelete": 50,
"DIDSet": 49,
"DepositPreauth": 19,

View File

@@ -5,9 +5,10 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## Unreleased Changes
### Added
* parseTransactionFlags as a utility function in the xrpl package to streamline transactions flags-to-map conversion
* 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

View File

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

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

@@ -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,6 +47,7 @@ type LedgerEntryFilter =
| 'amm'
| 'bridge'
| 'check'
| 'credential'
| 'deposit_preauth'
| 'did'
| 'directory'

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'
@@ -41,6 +42,7 @@ export {
AMM,
Bridge,
Check,
Credential,
DepositPreauth,
DirectoryNode,
DID,

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

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

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

@@ -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,9 +1,12 @@
/* 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,
@@ -14,6 +17,9 @@ import {
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) {
@@ -61,6 +67,7 @@ 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'
@@ -121,6 +128,22 @@ 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.
*
@@ -387,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

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

View File

@@ -12,6 +12,7 @@ import {
validateOptionalField,
isNumber,
Account,
validateCredentialsList,
} from './common'
import type { TransactionMetadataBase } from './metadata'
@@ -149,6 +150,11 @@ export interface Payment extends BaseTransaction {
* field names are lower-case.
*/
DeliverMin?: Amount | MPTAmount
/**
* Credentials associated with the sender of this transaction.
* The credentials included must not be expired.
*/
CredentialIDs?: string[]
Flags?: number | PaymentFlagsInterface
}
@@ -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'
@@ -122,6 +125,9 @@ export type SubmittableTransaction =
| CheckCash
| CheckCreate
| Clawback
| CredentialAccept
| CredentialCreate
| CredentialDelete
| DIDDelete
| DIDSet
| DepositPreauth
@@ -299,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

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

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

@@ -135,7 +135,7 @@ describe('Payment', function () {
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
const mptID = meta.mpt_issuance_id!
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
@@ -151,7 +151,7 @@ describe('Payment', function () {
const authTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
MPTokenIssuanceID: mptID,
}
await testTransaction(testContext.client, authTx, wallet2)
@@ -173,7 +173,7 @@ describe('Payment', function () {
Account: testContext.wallet.classicAddress,
Destination: wallet2.classicAddress,
Amount: {
mpt_issuance_id: mptID!,
mpt_issuance_id: mptID,
value: '100',
},
}

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

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

@@ -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'
@@ -272,4 +273,107 @@ describe('Payment', function () {
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,
)
})
})