diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 291f87b730..8adf8af9c3 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -178,3 +178,15 @@ PriceOracle fixEmptyDID fixXChainRewardRounding fixPreviousTxnID +fixAMMv1_1 +Credentials +NFTokenMintOffer +fixNFTokenPageLinks +fixInnerObjTemplate2 +fixEnforceNFTokenTrustline +fixReducedOffersV2 +InvariantsV1_1 +MPTokensV1 +AMMv1_2 +AMMClawback +PermissionedDomains diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 797be9ce21..c88dcb9cd1 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -53,6 +53,8 @@ "AMM": 121, "DID": 73, "Oracle": 128, + "Credential": 129, + "PermissionedDomain": 130, "Any": -3, "Child": -2, "Nickname": 110, @@ -1070,6 +1072,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", { @@ -1430,6 +1452,16 @@ "type": "Hash256" } ], + [ + "DomainID", + { + "nth": 34, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "Amount", { @@ -1980,6 +2012,16 @@ "type": "Blob" } ], + [ + "CredentialType", + { + "nth": 31, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2140,6 +2182,16 @@ "type": "AccountID" } ], + [ + "Subject", + { + "nth": 24, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "Indexes", { @@ -2180,6 +2232,16 @@ "type": "Vector256" } ], + [ + "CredentialIDs", + { + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], [ "Paths", { @@ -2550,6 +2612,16 @@ "type": "STObject" } ], + [ + "Credential", + { + "nth": 33, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2739,6 +2811,35 @@ "isSigningField": true, "type": "STArray" } + ], + [ + "AuthorizeCredentials", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "UnauthorizeCredentials", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "AcceptedCredentials", { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } ] ], "TRANSACTION_RESULTS": { @@ -2923,7 +3024,8 @@ "tecINVALID_UPDATE_TIME": 188, "tecTOKEN_PAIR_NOT_FOUND": 189, "tecARRAY_EMPTY": 190, - "tecARRAY_TOO_LARGE": 191 + "tecARRAY_TOO_LARGE": 191, + "tecBAD_CREDENTIALS": 193 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2974,6 +3076,11 @@ "DIDDelete": 50, "OracleSet": 51, "OracleDelete": 52, + "CredentialCreate": 58, + "CredentialAccept": 59, + "CredentialDelete": 60, + "PermissionedDomainSet": 61, + "PermissionedDomainDelete": 62, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index da384a908c..912ab93e44 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -6,6 +6,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ### Added * parseTransactionFlags as a utility function in the xrpl package to streamline transactions flags-to-map conversion +* Implementation of XLS-80d PermissionedDomain feature. ### Fixed * `TransactionStream` model supports APIv2 diff --git a/packages/xrpl/src/models/ledger/LedgerEntry.ts b/packages/xrpl/src/models/ledger/LedgerEntry.ts index 1f8f3ec32b..e48b53d7e4 100644 --- a/packages/xrpl/src/models/ledger/LedgerEntry.ts +++ b/packages/xrpl/src/models/ledger/LedgerEntry.ts @@ -12,6 +12,7 @@ import NegativeUNL from './NegativeUNL' import Offer from './Offer' import Oracle from './Oracle' import PayChannel from './PayChannel' +import PermissionedDomain from './PermissionedDomain' import RippleState from './RippleState' import SignerList from './SignerList' import Ticket from './Ticket' @@ -33,6 +34,7 @@ type LedgerEntry = | Offer | Oracle | PayChannel + | PermissionedDomain | RippleState | SignerList | Ticket @@ -56,6 +58,7 @@ type LedgerEntryFilter = | 'offer' | 'oracle' | 'payment_channel' + | 'permissioned_domain' | 'signer_list' | 'state' | 'ticket' diff --git a/packages/xrpl/src/models/ledger/PermissionedDomain.ts b/packages/xrpl/src/models/ledger/PermissionedDomain.ts new file mode 100644 index 0000000000..abdc27caa2 --- /dev/null +++ b/packages/xrpl/src/models/ledger/PermissionedDomain.ts @@ -0,0 +1,23 @@ +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry' + +// Keshava TODO: After the merge of VerifiableCredentials feature, import this interface +export interface Credential { + Credential : { + Issuer: string + CredentialType: string + } +} + +export default interface PermissionedDomain extends BaseLedgerEntry, HasPreviousTxnID { + LedgerEntryType: 'PermissionedDomain' + + Owner: string + + Flags: 0 + + OwnerNode: string + + Sequence: number + + AcceptedCredentials: Credential[] +} diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index c7a8120758..0bd32910f1 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -86,3 +86,6 @@ export { XChainModifyBridgeFlags, XChainModifyBridgeFlagsInterface, } from './XChainModifyBridge' + +export {PermissionedDomainSet} from './permissionedDomainSet' +export {PermissionedDomainDelete} from './permissionedDomainDelete' diff --git a/packages/xrpl/src/models/transactions/permissionedDomainDelete.ts b/packages/xrpl/src/models/transactions/permissionedDomainDelete.ts new file mode 100644 index 0000000000..4391aaaf1b --- /dev/null +++ b/packages/xrpl/src/models/transactions/permissionedDomainDelete.ts @@ -0,0 +1,18 @@ +import { + BaseTransaction, + isString, + validateBaseTransaction, + validateRequiredField +} from './common' + +export interface PermissionedDomainDelete extends BaseTransaction { + TransactionType: 'PermissionedDomainDelete' + + DomainID: string +} + +export function validatePermissionedDomainDelete(tx: Record): void { + validateBaseTransaction(tx) + + validateRequiredField(tx, 'DomainID', isString) +} diff --git a/packages/xrpl/src/models/transactions/permissionedDomainSet.ts b/packages/xrpl/src/models/transactions/permissionedDomainSet.ts new file mode 100644 index 0000000000..92f962c218 --- /dev/null +++ b/packages/xrpl/src/models/transactions/permissionedDomainSet.ts @@ -0,0 +1,49 @@ +import { ValidationError } from '../../errors' + +import { + BaseTransaction, + isString, + validateBaseTransaction, + validateOptionalField, + validateRequiredField, +} from './common' + +import {Credential} from '../ledger/PermissionedDomain' + +const ACCEPTED_CREDENTIALS_MAX_LENGTH = 10 + +export interface PermissionedDomainSet extends BaseTransaction { + TransactionType: 'PermissionedDomainSet' + + DomainID?: string + AcceptedCredentials: Credential[] +} + +// eslint-disable-next-line max-lines-per-function -- necessary to validate many fields +export function validatePermissionedDomainSet(tx: Record): void { + validateBaseTransaction(tx) + + validateOptionalField(tx, 'DomainID', isString) + + // eslint-disable-next-line max-lines-per-function -- necessary to validate many fields + validateRequiredField(tx, 'AcceptedCredentials', (value) => { + if (!Array.isArray(value)) { + throw new ValidationError('PermissionedDomainSet: AcceptedCredentials must be an array') + } + + if (value.length > ACCEPTED_CREDENTIALS_MAX_LENGTH) { + throw new ValidationError( + `PermissionedDomainSet: AcceptedCredentials must have at most ${ACCEPTED_CREDENTIALS_MAX_LENGTH} Credential objects`, + ) + } + else if (value.length == 0) { + throw new ValidationError( + `PermissionedDomainSet: AcceptedCredentials must have at least one Credential object`, + ) + } + + // Note: This implementation does not rigorously validate the inner-object format of AcceptedCredentials array because that would be a blatant repetition of the rippled cpp implementation. + + return true + }) +} diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index 0ddc719539..2b8eb3ce7c 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -58,6 +58,8 @@ import { PaymentChannelFund, validatePaymentChannelFund, } from './paymentChannelFund' +import { PermissionedDomainDelete, validatePermissionedDomainDelete } from './permissionedDomainDelete' +import { PermissionedDomainSet, validatePermissionedDomainSet } from './permissionedDomainSet' import { SetFee } from './setFee' import { SetRegularKey, validateSetRegularKey } from './setRegularKey' import { SignerListSet, validateSignerListSet } from './signerListSet' @@ -128,6 +130,8 @@ export type SubmittableTransaction = | PaymentChannelClaim | PaymentChannelCreate | PaymentChannelFund + | PermissionedDomainSet + | PermissionedDomainDelete | SetRegularKey | SignerListSet | TicketCreate @@ -358,6 +362,14 @@ export function validate(transaction: Record): void { validatePaymentChannelFund(tx) break + case 'PermissionedDomainSet': + validatePermissionedDomainSet(tx) + break + + case 'PermissionedDomainDelete': + validatePermissionedDomainDelete(tx) + break + case 'SetRegularKey': validateSetRegularKey(tx) break diff --git a/packages/xrpl/test/integration/transactions/permissionedDomain.test.ts b/packages/xrpl/test/integration/transactions/permissionedDomain.test.ts new file mode 100644 index 0000000000..abf92a8c04 --- /dev/null +++ b/packages/xrpl/test/integration/transactions/permissionedDomain.test.ts @@ -0,0 +1,81 @@ +import { stringToHex } from '@xrplf/isomorphic/utils' +import { assert } from 'chai' + +import { LedgerEntryRequest, PermissionedDomainDelete, PermissionedDomainSet } from '../../../src' +import PermissionedDomain, { Credential } from '../../../src/models/ledger/PermissionedDomain' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('PermissionedDomainSet', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'Lifecycle of PermissionedDomain ledger object', + async () => { + const sampleCredential: Credential = { + Credential: { + CredentialType: stringToHex('Passport'), + Issuer: testContext.wallet.classicAddress + } + } + + // Step-1: Test the PermissionedDomainSet transaction + const tx_pd_set: PermissionedDomainSet = { + TransactionType: 'PermissionedDomainSet', + Account: testContext.wallet.classicAddress, + AcceptedCredentials: [sampleCredential] + } + + await testTransaction(testContext.client, tx_pd_set, testContext.wallet) + + // Step-2: Validate the ledger_entry, account_objects RPC methods + // validate the account_objects RPC + const result = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'permissioned_domain', + }) + + assert.equal(result.result.account_objects.length, 1) + const pd = result.result.account_objects[0] as PermissionedDomain + + assert.equal(pd.Flags, 0) + expect(pd.AcceptedCredentials).toEqual([sampleCredential]) + + // validate the ledger_entry RPC + const ledger_entry_request: LedgerEntryRequest = { + command: 'ledger_entry', + // fetch the PD `index` from the previous account_objects RPC response + index: pd.index + } + const ledger_entry_result = await testContext.client.request(ledger_entry_request) + assert.deepEqual(pd, ledger_entry_result.result.node) + + // Step-3: Test the PDDelete transaction + const tx_pd_delete: PermissionedDomainDelete = { + TransactionType: 'PermissionedDomainDelete', + Account: testContext.wallet.classicAddress, + // fetch the PD `index` from the previous account_objects RPC response + DomainID: pd.index + } + + console.log(pd) + + await testTransaction(testContext.client, tx_pd_delete, testContext.wallet) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/models/permissionedDomainDelete.test.ts b/packages/xrpl/test/models/permissionedDomainDelete.test.ts new file mode 100644 index 0000000000..dd651dc6ca --- /dev/null +++ b/packages/xrpl/test/models/permissionedDomainDelete.test.ts @@ -0,0 +1,40 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validatePermissionedDomainDelete } from '../../src/models/transactions/permissionedDomainDelete' + +/** + * PermissionedDomainDelete Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('PermissionedDomainDelete', function () { + let tx + + beforeEach(function () { + tx = { + TransactionType: 'PermissionedDomainDelete', + Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8', + DomainID: 'D88930B33C2B6831660BFD006D91FF100011AD4E67CBB78B460AF0A215103737', + } as any + }) + + it('verifies valid PermissionedDomainDelete', function () { + assert.doesNotThrow(() => validatePermissionedDomainDelete(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it(`throws w/ missing field DomainID`, function () { + delete tx.DomainID + const errorMessage = 'PermissionedDomainDelete: missing field DomainID' + assert.throws(() => validatePermissionedDomainDelete(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) + + it(`throws w/ invalid DomainID`, function () { + tx.DomainID = 1234 + const errorMessage = 'PermissionedDomainDelete: invalid field DomainID' + assert.throws(() => validatePermissionedDomainDelete(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/models/permissionedDomainSet.test.ts b/packages/xrpl/test/models/permissionedDomainSet.test.ts new file mode 100644 index 0000000000..b9dbc907a3 --- /dev/null +++ b/packages/xrpl/test/models/permissionedDomainSet.test.ts @@ -0,0 +1,51 @@ +import { stringToHex } from '@xrplf/isomorphic/dist/utils' +import { assert } from 'chai' + +import { Credential } from '../../src/models/ledger/PermissionedDomain' +import { validatePermissionedDomainSet } from '../../src/models/transactions/permissionedDomainSet' +import { validate, ValidationError } from '../../src' + +/** + * PermissionedDomainSet Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('PermissionedDomainSet', function () { + let tx + + beforeEach(function () { + + const sampleCredential: Credential = { + Credential: { + CredentialType: stringToHex('Passport'), + Issuer: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8' + } + } + + tx = { + TransactionType: 'PermissionedDomainSet', + Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8', + DomainID: 'D88930B33C2B6831660BFD006D91FF100011AD4E67CBB78B460AF0A215103737', + AcceptedCredentials: [sampleCredential] + } as any + }) + + it('verifies valid PermissionedDomainSet', function () { + assert.doesNotThrow(() => validatePermissionedDomainSet(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it(`throws with invalid field DomainID`, function () { + tx.DomainID = 1234 // DomainID is expected to be a string + const errorMessage = 'PermissionedDomainSet: invalid field DomainID' + assert.throws(() => validatePermissionedDomainSet(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) + + it(`throws w/ missing field AcceptedCredentials`, function () { + delete tx.AcceptedCredentials + const errorMessage = 'PermissionedDomainSet: missing field AcceptedCredentials' + assert.throws(() => validatePermissionedDomainSet(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) +})