diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 8ac86fa558..f4b13336e6 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -167,5 +167,6 @@ AMM Clawback fixReducedOffersV1 fixNFTokenRemint -# 2.0.0-b1 Amendments +# 2.0.0 Amendments XChainBridge +DID diff --git a/packages/ripple-binary-codec/HISTORY.md b/packages/ripple-binary-codec/HISTORY.md index d14778149e..cf02af54dc 100644 --- a/packages/ripple-binary-codec/HISTORY.md +++ b/packages/ripple-binary-codec/HISTORY.md @@ -1,10 +1,12 @@ # ripple-binary-codec Release History ## Unreleased +### Added +- Support for the DID amendment (XLS-40). ## 1.10.0 (2023-09-27) ### Added -- Support for the XChainBridge amendment. +- Support for the XChainBridge amendment (XLS-38). ## 1.9.0 (2023-08-24) diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index b6b48f440c..b8fd9a8a1b 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -50,6 +50,7 @@ "NFTokenPage": 80, "NFTokenOffer": 55, "AMM": 121, + "DID": 73, "Any": -3, "Child": -2, "Nickname": 110, @@ -140,40 +141,40 @@ [ "LedgerEntry", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "LedgerEntry" } ], [ "Transaction", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "Transaction" } ], [ "Validation", { - "nth": 1, + "nth": 257, "isVLEncoded": false, "isSerialized": false, - "isSigningField": true, + "isSigningField": false, "type": "Validation" } ], [ "Metadata", { - "nth": 1, + "nth": 257, "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, + "isSerialized": false, + "isSigningField": false, "type": "Metadata" } ], @@ -1897,6 +1898,26 @@ "type": "Blob" } ], + [ + "DIDDocument", + { + "nth": 26, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "Data", + { + "nth": 27, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2681,6 +2702,7 @@ "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, + "temEMPTY_DID": -254, "tefFAILURE": -199, "tefALREADY": -198, @@ -2759,7 +2781,7 @@ "tecKILLED": 150, "tecHAS_OBLIGATIONS": 151, "tecTOO_SOON": 152, - "tecHOOK_ERROR": 153, + "tecHOOK_REJECTED": 153, "tecMAX_SEQUENCE_REACHED": 154, "tecNO_SUITABLE_NFTOKEN_PAGE": 155, "tecNFTOKEN_BUY_SELL_MISMATCH": 156, @@ -2792,7 +2814,8 @@ "tecXCHAIN_PAYMENT_FAILED": 183, "tecXCHAIN_SELF_COMMIT": 184, "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, - "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186 + "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186, + "tecEMPTY_DID": 187 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2839,6 +2862,8 @@ "XChainAddAccountCreateAttestation": 46, "XChainModifyBridge": 47, "XChainCreateBridge": 48, + "DIDSet": 49, + "DIDDelete": 50, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json b/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json index 9029084c5a..1f4f9616eb 100644 --- a/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json +++ b/packages/ripple-binary-codec/test/fixtures/codec-fixtures.json @@ -4841,6 +4841,33 @@ "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", "TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107" } + }, + { + "binary": "1200322280000000240000000468400000000000000A7321ED9861C4CB029C0DA737B823D7D3459A70F227958D5C0C111CC7CF947FC5A93347744071E28B12465A1B47162C22E121DF61089DCD9AAF5773704B76179E771666886C8AAD5A33A87E34CC381A7D924E3FE3645F0BF98D565DE42C81E1A7A7E7981802811401476926B590BA3245F63C829116A0A3AF7F382D", + "json": { + "Account": "rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8", + "Fee": "10", + "Flags": 2147483648, + "Sequence": 4, + "SigningPubKey": "ED9861C4CB029C0DA737B823D7D3459A70F227958D5C0C111CC7CF947FC5A93347", + "TransactionType": "DIDDelete", + "TxnSignature": "71E28B12465A1B47162C22E121DF61089DCD9AAF5773704B76179E771666886C8AAD5A33A87E34CC381A7D924E3FE3645F0BF98D565DE42C81E1A7A7E7981802" + } + }, + { + "binary": "1200312280000000240000000368400000000000000A7321ED9861C4CB029C0DA737B823D7D3459A70F227958D5C0C111CC7CF947FC5A933477440AACD31A04CAE14670FC483A1382F393AA96B49C84479B58067F049FBD772999325667A6AA2520A63756EE84F3657298815019DD56A1AECE796B08535C4009C08750B6469645F6578616D706C65701A03646F63701B06617474657374811401476926B590BA3245F63C829116A0A3AF7F382D", + "json": { + "Account": "rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8", + "Data": "617474657374", + "DIDDocument": "646F63", + "Fee": "10", + "Flags": 2147483648, + "Sequence": 3, + "SigningPubKey": "ED9861C4CB029C0DA737B823D7D3459A70F227958D5C0C111CC7CF947FC5A93347", + "TransactionType": "DIDSet", + "TxnSignature": "AACD31A04CAE14670FC483A1382F393AA96B49C84479B58067F049FBD772999325667A6AA2520A63756EE84F3657298815019DD56A1AECE796B08535C4009C08", + "URI": "6469645F6578616D706C65" + } } ], "ledgerData": [{ diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 56f485090f..f3ffda1fe4 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -3,6 +3,8 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xrpl-announce) for release announcements. We recommend that xrpl.js (ripple-lib) users stay up-to-date with the latest stable release. ## Unreleased +### Added +- Support for the DID amendment (XLS-40). ### Added * Support for `server_definitions` RPC diff --git a/packages/xrpl/src/models/ledger/LedgerEntry.ts b/packages/xrpl/src/models/ledger/LedgerEntry.ts index deb7d0136d..b2252dc5e4 100644 --- a/packages/xrpl/src/models/ledger/LedgerEntry.ts +++ b/packages/xrpl/src/models/ledger/LedgerEntry.ts @@ -44,6 +44,7 @@ type LedgerEntryFilter = | 'bridge' | 'check' | 'deposit_preauth' + | 'did' | 'directory' | 'escrow' | 'fee' diff --git a/packages/xrpl/src/models/transactions/DIDDelete.ts b/packages/xrpl/src/models/transactions/DIDDelete.ts new file mode 100644 index 0000000000..1cc2a4214d --- /dev/null +++ b/packages/xrpl/src/models/transactions/DIDDelete.ts @@ -0,0 +1,20 @@ +import { BaseTransaction, validateBaseTransaction } from './common' + +// TODO: add docs + +/** + * @category Transaction Models + */ +export interface DIDDelete extends BaseTransaction { + TransactionType: 'DIDDelete' +} + +/** + * Verify the form and type of a DIDDelete at runtime. + * + * @param tx - A DIDDelete Transaction. + * @throws When the DIDDelete is malformed. + */ +export function validateDIDDelete(tx: Record): void { + validateBaseTransaction(tx) +} diff --git a/packages/xrpl/src/models/transactions/DIDSet.ts b/packages/xrpl/src/models/transactions/DIDSet.ts new file mode 100644 index 0000000000..09db33bd1c --- /dev/null +++ b/packages/xrpl/src/models/transactions/DIDSet.ts @@ -0,0 +1,37 @@ +import { + BaseTransaction, + isString, + validateBaseTransaction, + validateOptionalField, +} from './common' + +// TODO: add docs + +/** + * @category Transaction Models + */ +export interface DIDSet extends BaseTransaction { + TransactionType: 'DIDSet' + + Data?: string + + DIDDocument?: string + + URI?: string +} + +/** + * Verify the form and type of a DIDSet at runtime. + * + * @param tx - A DIDSet Transaction. + * @throws When the DIDSet is malformed. + */ +export function validateDIDSet(tx: Record): void { + validateBaseTransaction(tx) + + validateOptionalField(tx, 'Data', isString) + + validateOptionalField(tx, 'DIDDocument', isString) + + validateOptionalField(tx, 'URI', isString) +} diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index 59e3ae87c2..66e9db3406 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -31,6 +31,8 @@ export { CheckCancel } from './checkCancel' export { CheckCash } from './checkCash' export { CheckCreate } from './checkCreate' export { Clawback } from './clawback' +export { DIDDelete } from './DIDDelete' +export { DIDSet } from './DIDSet' export { DepositPreauth } from './depositPreauth' export { EscrowCancel } from './escrowCancel' export { EscrowCreate } from './escrowCreate' diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index 575d58c003..68bf737c7c 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -20,6 +20,8 @@ import { CheckCreate, validateCheckCreate } from './checkCreate' import { Clawback, validateClawback } from './clawback' import { isIssuedCurrency } from './common' import { DepositPreauth, validateDepositPreauth } from './depositPreauth' +import { DIDDelete, validateDIDDelete } from './DIDDelete' +import { DIDSet, validateDIDSet } from './DIDSet' import { EnableAmendment } from './enableAmendment' import { EscrowCancel, validateEscrowCancel } from './escrowCancel' import { EscrowCreate, validateEscrowCreate } from './escrowCreate' @@ -91,18 +93,20 @@ import { * @category Transaction Models */ export type Transaction = - | AccountDelete - | AccountSet | AMMBid + | AMMCreate | AMMDelete | AMMDeposit - | AMMCreate | AMMVote | AMMWithdraw + | AccountDelete + | AccountSet | CheckCancel | CheckCash | CheckCreate | Clawback + | DIDDelete + | DIDSet | DepositPreauth | EscrowCancel | EscrowCreate @@ -122,13 +126,13 @@ export type Transaction = | SignerListSet | TicketCreate | TrustSet + | XChainAccountCreateCommit | XChainAddAccountCreateAttestation | XChainAddClaimAttestation | XChainClaim | XChainCommit | XChainCreateBridge | XChainCreateClaimID - | XChainAccountCreateCommit | XChainModifyBridge export type PseudoTransaction = EnableAmendment | SetFee | UNLModify @@ -210,18 +214,14 @@ export function validate(transaction: Record): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- okay here setTransactionFlagsToNumber(tx as unknown as Transaction) switch (tx.TransactionType) { - case 'AccountDelete': - validateAccountDelete(tx) - break - - case 'AccountSet': - validateAccountSet(tx) - break - case 'AMMBid': validateAMMBid(tx) break + case 'AMMCreate': + validateAMMCreate(tx) + break + case 'AMMDelete': validateAMMDelete(tx) break @@ -230,10 +230,6 @@ export function validate(transaction: Record): void { validateAMMDeposit(tx) break - case 'AMMCreate': - validateAMMCreate(tx) - break - case 'AMMVote': validateAMMVote(tx) break @@ -242,6 +238,14 @@ export function validate(transaction: Record): void { validateAMMWithdraw(tx) break + case 'AccountDelete': + validateAccountDelete(tx) + break + + case 'AccountSet': + validateAccountSet(tx) + break + case 'CheckCancel': validateCheckCancel(tx) break @@ -258,6 +262,14 @@ export function validate(transaction: Record): void { validateClawback(tx) break + case 'DIDDelete': + validateDIDDelete(tx) + break + + case 'DIDSet': + validateDIDSet(tx) + break + case 'DepositPreauth': validateDepositPreauth(tx) break @@ -334,6 +346,10 @@ export function validate(transaction: Record): void { validateTrustSet(tx) break + case 'XChainAccountCreateCommit': + validateXChainAccountCreateCommit(tx) + break + case 'XChainAddAccountCreateAttestation': validateXChainAddAccountCreateAttestation(tx) break @@ -358,10 +374,6 @@ export function validate(transaction: Record): void { validateXChainCreateClaimID(tx) break - case 'XChainAccountCreateCommit': - validateXChainAccountCreateCommit(tx) - break - case 'XChainModifyBridge': validateXChainModifyBridge(tx) break diff --git a/packages/xrpl/test/integration/transactions/didDelete.test.ts b/packages/xrpl/test/integration/transactions/didDelete.test.ts new file mode 100644 index 0000000000..0b5b6caa5c --- /dev/null +++ b/packages/xrpl/test/integration/transactions/didDelete.test.ts @@ -0,0 +1,70 @@ +import { assert } from 'chai' + +import { DIDSet, DIDDelete } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('DIDDelete', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const setupTx: DIDSet = { + TransactionType: 'DIDSet', + Account: testContext.wallet.address, + Data: '617474657374', + DIDDocument: '646F63', + URI: '6469645F6578616D706C65', + } + + await testTransaction(testContext.client, setupTx, testContext.wallet) + + // double check the DID was properly created + const initialAccountOffersResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.address, + type: 'did', + }) + assert.lengthOf( + initialAccountOffersResponse.result.account_objects, + 1, + 'Should be exactly one DID on the ledger after a DIDSet transaction', + ) + + // actual test - cancel the check + const tx: DIDDelete = { + TransactionType: 'DIDDelete', + Account: testContext.wallet.address, + } + + await testTransaction(testContext.client, tx, testContext.wallet) + + // confirm that the DID no longer exists + const accountOffersResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.address, + type: 'did', + }) + assert.lengthOf( + accountOffersResponse.result.account_objects, + 0, + 'Should be no DID on the ledger after a DIDDelete transaction', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/integration/transactions/didSet.test.ts b/packages/xrpl/test/integration/transactions/didSet.test.ts new file mode 100644 index 0000000000..88b0854c00 --- /dev/null +++ b/packages/xrpl/test/integration/transactions/didSet.test.ts @@ -0,0 +1,50 @@ +import { assert } from 'chai' + +import { DIDSet } from '../../../src' +import serverUrl from '../serverUrl' +import { + setupClient, + teardownClient, + type XrplIntegrationTestContext, +} from '../setup' +import { testTransaction } from '../utils' + +// how long before each test case times out +const TIMEOUT = 20000 + +describe('DIDSet', function () { + let testContext: XrplIntegrationTestContext + + beforeEach(async () => { + testContext = await setupClient(serverUrl) + }) + afterEach(async () => teardownClient(testContext)) + + it( + 'base', + async () => { + const tx: DIDSet = { + TransactionType: 'DIDSet', + Account: testContext.wallet.classicAddress, + Data: '617474657374', + DIDDocument: '646F63', + URI: '6469645F6578616D706C65', + } + + await testTransaction(testContext.client, tx, testContext.wallet) + + // confirm that the DID was actually created + const accountOffersResponse = await testContext.client.request({ + command: 'account_objects', + account: testContext.wallet.classicAddress, + type: 'did', + }) + assert.lengthOf( + accountOffersResponse.result.account_objects, + 1, + 'Should be exactly one DID on the ledger after a DIDSet transaction', + ) + }, + TIMEOUT, + ) +}) diff --git a/packages/xrpl/test/models/DIDDelete.test.ts b/packages/xrpl/test/models/DIDDelete.test.ts new file mode 100644 index 0000000000..7248579b43 --- /dev/null +++ b/packages/xrpl/test/models/DIDDelete.test.ts @@ -0,0 +1,34 @@ +import { assert } from 'chai' + +import { validate } from '../../src' +import { validateDIDDelete } from '../../src/models/transactions/DIDDelete' + +/** + * DIDDelete Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('DIDDelete', function () { + let tx + + beforeEach(function () { + tx = { + Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8', + Fee: '10', + Flags: 2147483648, + Sequence: 4, + TransactionType: 'DIDDelete', + } as any + }) + + it('verifies valid DIDDelete', function () { + assert.doesNotThrow(() => validateDIDDelete(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it('throws on invalid DIDDelete', function () { + tx.FakeField = 'blah' + assert.doesNotThrow(() => validateDIDDelete(tx)) + assert.doesNotThrow(() => validate(tx)) + }) +}) diff --git a/packages/xrpl/test/models/DIDSet.test.ts b/packages/xrpl/test/models/DIDSet.test.ts new file mode 100644 index 0000000000..8a35175068 --- /dev/null +++ b/packages/xrpl/test/models/DIDSet.test.ts @@ -0,0 +1,76 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateDIDSet } from '../../src/models/transactions/DIDSet' + +/** + * DIDSet Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('DIDSet', function () { + let tx + + beforeEach(function () { + tx = { + Account: 'rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8', + Data: '617474657374', + DIDDocument: '646F63', + Fee: '10', + Flags: 2147483648, + Sequence: 3, + TransactionType: 'DIDSet', + URI: '6469645F6578616D706C65', + } as any + }) + + it('verifies valid DIDSet', function () { + assert.doesNotThrow(() => validateDIDSet(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it('throws w/ invalid Data', function () { + tx.Data = 123 + + assert.throws( + () => validateDIDSet(tx), + ValidationError, + 'DIDSet: invalid field Data', + ) + assert.throws( + () => validate(tx), + ValidationError, + 'DIDSet: invalid field Data', + ) + }) + + it('throws w/ invalid DIDDocument', function () { + tx.DIDDocument = 123 + + assert.throws( + () => validateDIDSet(tx), + ValidationError, + 'DIDSet: invalid field DIDDocument', + ) + assert.throws( + () => validate(tx), + ValidationError, + 'DIDSet: invalid field DIDDocument', + ) + }) + + it('throws w/ invalid URI', function () { + tx.URI = 123 + + assert.throws( + () => validateDIDSet(tx), + ValidationError, + 'DIDSet: invalid field URI', + ) + assert.throws( + () => validate(tx), + ValidationError, + 'DIDSet: invalid field URI', + ) + }) +}) diff --git a/packages/xrpl/tools/createValidate.js b/packages/xrpl/tools/createValidate.js index 05c1499a1e..435a5bb52f 100644 --- a/packages/xrpl/tools/createValidate.js +++ b/packages/xrpl/tools/createValidate.js @@ -4,19 +4,18 @@ * folder. */ const fs = require('fs') +const path = require('path') const NORMAL_TYPES = ['number', 'string'] const NUMBERS = ['0', '1'] // TODO: rewrite this to use regex -async function main() { - if (process.argv.length < 3) { - console.log(`Usage: ${process.argv[0]} ${process.argv[1]} TxName`) - process.exit(1) - } - const modelName = process.argv[2] - const filename = `./src/models/transactions/${modelName}.ts` +async function main(modelName) { + const filename = path.join( + path.dirname(__filename), + `../src/models/transactions/${modelName}.ts`, + ) const [model, txName] = await getModel(filename) return processModel(model, txName) } @@ -144,4 +143,12 @@ ${output}` return output } -main().then(console.log) +if (require.main === module) { + if (process.argv.length < 3) { + console.log(`Usage: ${process.argv[0]} ${process.argv[1]} TxName`) + process.exit(1) + } + main(process.argv[2]).then(console.log) +} + +module.exports = main diff --git a/packages/xrpl/tools/createValidateTests.js b/packages/xrpl/tools/createValidateTests.js index a2db61bd40..f88e5da114 100644 --- a/packages/xrpl/tools/createValidateTests.js +++ b/packages/xrpl/tools/createValidateTests.js @@ -1,5 +1,6 @@ const fs = require('fs') -const fixtures = require('ripple-binary-codec/test/fixtures/codec-fixtures.json') +const path = require('path') +const fixtures = require('../../ripple-binary-codec/test/fixtures/codec-fixtures.json') const NORMAL_TYPES = ['number', 'string'] const NUMBERS = ['0', '1'] @@ -9,15 +10,20 @@ function getTx(txName) { const validTxs = fixtures.transactions .filter((tx) => tx.json.TransactionType === txName) .map((tx) => tx.json) + if (validTxs.length == 0) { + throw new Error(`Must have ripple-binary-codec fixture for ${txName}`) + } const validTx = validTxs[0] delete validTx.TxnSignature delete validTx.SigningPubKey return JSON.stringify(validTx, null, 2) } -function main() { - const modelName = process.argv[2] - const filename = `./packages/xrpl/src/models/transactions/${modelName}.ts` +function main(modelName) { + const filename = path.join( + path.dirname(__filename), + `../src/models/transactions/${modelName}.ts`, + ) const [model, txName] = getModel(filename) return processModel(model, txName) } @@ -61,6 +67,8 @@ function getInvalidValue(paramTypes) { return 123 } else if (paramType == 'IssuedCurrency') { return JSON.stringify({ test: 'test' }) + } else if (paramType == 'Currency') { + return JSON.stringify({ test: 'test' }) } else if (paramType == 'Amount') { return JSON.stringify({ currency: 'ETH' }) } else if (paramType == 'XChainBridge') { @@ -184,4 +192,12 @@ describe('${txName}', function () { return output } -console.log(main()) +if (require.main === module) { + if (process.argv.length < 3) { + console.log(`Usage: ${process.argv[0]} ${process.argv[1]} TxName`) + process.exit(1) + } + console.log(main(process.argv[2])) +} + +module.exports = main diff --git a/packages/xrpl/tools/generateModels.js b/packages/xrpl/tools/generateModels.js new file mode 100644 index 0000000000..cd089dfaca --- /dev/null +++ b/packages/xrpl/tools/generateModels.js @@ -0,0 +1,273 @@ +/** + * A script that generates models and model unit tests. + * To run it, clone the rippled branch with the source code and run this script against that repo. + */ +const fs = require('fs') +const path = require('path') +const createValidate = require('./createValidate') +const createValidateTests = require('./createValidateTests') + +function readFile(filename) { + return fs.readFileSync(filename, 'utf-8') +} + +let jsTransactionFile + +function processRippledSource(folder) { + const sfieldCpp = readFile( + path.join(folder, 'src/ripple/protocol/impl/SField.cpp'), + ) + const sfieldHits = sfieldCpp.match( + /^ *CONSTRUCT_[^\_]+_SFIELD *\( *[^,\n]*,[ \n]*"([^\"\n ]+)"[ \n]*,[ \n]*([^, \n]+)[ \n]*,[ \n]*([0-9]+)(,.*?(notSigning))?/gm, + ) + const sfields = {} + for (const hit of sfieldHits) { + const matches = hit.match( + /^ *CONSTRUCT_[^\_]+_SFIELD *\( *[^,\n]*,[ \n]*"([^\"\n ]+)"[ \n]*,[ \n]*([^, \n]+)[ \n]*,[ \n]*([0-9]+)(,.*?(notSigning))?/, + ) + sfields[matches[1]] = matches.slice(2) + } + + const txFormatsCpp = readFile( + path.join(folder, 'src/ripple/protocol/impl/TxFormats.cpp'), + ) + const txFormatsHits = txFormatsCpp.match( + /^ *add\(jss::([^\"\n, ]+),[ \n]*tt[A-Z_]+,[ \n]*{[ \n]*(({sf[A-Za-z0-9]+, soe(OPTIONAL|REQUIRED|DEFAULT)},[ \n]+)*)},[ \n]*[pseudocC]+ommonFields\);/gm, + ) + const txFormats = {} + for (const hit of txFormatsHits) { + const matches = hit.match( + /^ *add\(jss::([^\"\n, ]+),[ \n]*tt[A-Z_]+,[ \n]*{[ \n]*(({sf[A-Za-z0-9]+, soe(OPTIONAL|REQUIRED|DEFAULT)},[ \n]+)*)},[ \n]*[pseudocC]+ommonFields\);/, + ) + txFormats[matches[1]] = formatTxFormat(matches[2]) + } + + jsTransactionFile = readFile( + path.join( + path.dirname(__filename), + '../src/models/transactions/transaction.ts', + ), + ) + const transactionMatch = jsTransactionFile.match( + /export type Transaction =([| \nA-Za-z]+)\nexport/, + )[0] + const existingLibraryTxs = transactionMatch + .replace('\n\nexport', '') + .split('\n | ') + .filter((value) => !value.includes('export type')) + .map((value) => value.trim()) + existingLibraryTxs.push('EnableAmendment', 'SetFee', 'UNLModify') + + const txsToAdd = [] + + for (const tx in txFormats) { + if (!existingLibraryTxs.includes(tx)) { + txsToAdd.push(tx) + } + } + + return [txsToAdd, txFormats, sfields, transactionMatch] +} + +function formatTxFormat(rawTxFormat) { + return rawTxFormat + .trim() + .split('\n') + .map((element) => element.trim().replace(/[{},]/g, '').split(' ')) +} + +const typeMap = { + UINT8: 'number', + UINT16: 'number', + UINT32: 'number', + UINT64: 'number | string', + UINT128: 'string', + UINT160: 'string', + UINT256: 'string', + AMOUNT: 'Amount', + VL: 'string', + ACCOUNT: 'string', + VECTOR256: 'string[]', + PATHSET: 'Path[]', + ISSUE: 'Currency', + XCHAIN_BRIDGE: 'XChainBridge', + OBJECT: 'any', + ARRAY: 'any[]', +} + +const allCommonImports = ['Amount', 'Currency', 'Path', 'XChainBridge'] +const additionalValidationImports = ['string', 'number'] + +function updateTransactionFile(transactionMatch, tx) { + const transactionMatchSplit = transactionMatch.split('\n | ') + const firstLine = transactionMatchSplit[0] + const allTransactions = transactionMatchSplit.slice(1) + allTransactions.push(tx) + allTransactions.sort() + const newTransactionMatch = + firstLine + '\n | ' + allTransactions.join('\n | ') + let newJsTxFile = jsTransactionFile.replace( + transactionMatch, + newTransactionMatch, + ) + + // Adds the imports to the end of the imports + newJsTxFile = newJsTxFile.replace( + `import { + XChainModifyBridge, + validateXChainModifyBridge, +} from './XChainModifyBridge'`, + `import { + XChainModifyBridge, + validateXChainModifyBridge, +} from './XChainModifyBridge' +import { + ${tx}, + validate${tx}, +} from './${tx}'`, + ) + + const validationMatch = newJsTxFile.match( + /switch \(tx.TransactionType\) {\n([ \nA-Za-z':()]+)default/, + )[1] + const caseValidations = validationMatch.split('\n\n') + caseValidations.push( + ` case '${tx}':\n validate${tx}(tx)\n break`, + ) + caseValidations.sort() + newJsTxFile = newJsTxFile.replace( + validationMatch, + caseValidations.join('\n\n') + '\n\n ', + ) + + fs.writeFileSync( + path.join( + path.dirname(__filename), + '../src/models/transactions/transaction.ts', + ), + newJsTxFile, + ) + + transactionMatch = newTransactionMatch + jsTransactionFile = newJsTxFile +} + +function updateIndexFile(tx) { + const filename = path.join( + path.dirname(__filename), + '../src/models/transactions/index.ts', + ) + let indexFile = readFile(filename) + indexFile = indexFile.replace( + `} from './XChainModifyBridge'`, + `} from './XChainModifyBridge' +export { ${tx} } from './${tx}'`, + ) + fs.writeFileSync(filename, indexFile) +} + +function generateParamLine(sfields, param, isRequired) { + const paramName = param.slice(2) + const paramType = sfields[paramName][0] + const paramTypeOutput = typeMap[paramType] + return ` ${paramName}${isRequired ? '' : '?'}: ${paramTypeOutput}\n` +} + +async function main(folder) { + const [txsToAdd, txFormats, sfields, transactionMatch] = + processRippledSource(folder) + txsToAdd.forEach(async (tx) => { + const txFormat = txFormats[tx] + const paramLines = txFormat + .filter((param) => param[0] !== '') + .sort((a, b) => a[0].localeCompare(b[0])) + .map((param) => + generateParamLine(sfields, param[0], param[1] === 'soeREQUIRED'), + ) + paramLines.sort((a, b) => !a.includes('REQUIRED')) + const params = paramLines.join('\n') + let model = `/** + * @category Transaction Models + */ +export interface ${tx} extends BaseTransaction { + TransactionType: '${tx}' + +${params} +}` + + const commonImports = [] + const validationImports = ['BaseTransaction', 'validateBaseTransaction'] + for (const item of allCommonImports) { + if (params.includes(item)) { + commonImports.push(item) + validationImports.push('is' + item) + } + } + for (const item of additionalValidationImports) { + if (params.includes(item)) { + validationImports.push( + 'is' + item.substring(0, 1).toUpperCase() + item.substring(1), + ) + } + } + if (params.includes('?')) { + validationImports.push('validateOptionalField') + } + if (/[A-Za-z0-9]+:/.test(params)) { + validationImports.push('validateRequiredField') + } + validationImports.sort() + const commonImportLine = + commonImports.length > 0 + ? `import { ${commonImports.join(', ')} } from '../common'` + : '' + const validationImportLine = `import { ${validationImports.join( + ', ', + )} } from './common'` + let imported_models = `${commonImportLine} + +${validationImportLine}` + imported_models = imported_models.replace('\n\n\n\n', '\n\n') + imported_models = imported_models.replace('\n\n\n', '\n\n') + model = model.replace('\n\n\n\n', '\n\n') + fs.writeFileSync( + path.join( + path.dirname(__filename), + `../src/models/transactions/${tx}.ts`, + ), + imported_models + '\n\n' + model, + ) + + const validate = await createValidate(tx) + fs.appendFileSync( + path.join( + path.dirname(__filename), + `../src/models/transactions/${tx}.ts`, + ), + '\n\n' + validate, + ) + + const validateTests = createValidateTests(tx) + fs.writeFileSync( + path.join(path.dirname(__filename), `../test/models/${tx}.test.ts`), + validateTests, + ) + + updateTransactionFile(transactionMatch, tx) + + updateIndexFile(tx) + + console.log(`Added ${tx}`) + }) + console.log( + 'Future steps: Adding docstrings to the models and adding integration tests', + ) +} + +if (require.main === module) { + if (process.argv.length < 3) { + console.log(`Usage: ${process.argv[0]} ${process.argv[1]} path/to/rippled`) + process.exit(1) + } + main(process.argv[2]) +}