Files
xahau.js/packages/xrpl/tools/generateModels.js
Mayukha Vadari c8f25a6347 feat: add support for XLS-40d + add script to auto-generate models from rippled code (#2491)
Add support for XLS-40 and adds a script to automatically
generate transaction models from rippled source code.

### Context of Change

https://github.com/XRPLF/XRPL-Standards/pull/136
https://github.com/XRPLF/rippled/pull/4636
2023-11-15 16:19:50 -06:00

274 lines
7.7 KiB
JavaScript

/**
* 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])
}