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
This commit is contained in:
Mayukha Vadari
2023-11-15 17:19:50 -05:00
committed by GitHub
parent 22dd17d6b7
commit c8f25a6347
17 changed files with 701 additions and 46 deletions

View File

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

View File

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

View File

@@ -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": [{

View File

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

View File

@@ -44,6 +44,7 @@ type LedgerEntryFilter =
| 'bridge'
| 'check'
| 'deposit_preauth'
| 'did'
| 'directory'
| 'escrow'
| 'fee'

View File

@@ -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<string, unknown>): void {
validateBaseTransaction(tx)
}

View File

@@ -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<string, unknown>): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'Data', isString)
validateOptionalField(tx, 'DIDDocument', isString)
validateOptionalField(tx, 'URI', isString)
}

View File

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

View File

@@ -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<string, unknown>): 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<string, unknown>): void {
validateAMMDeposit(tx)
break
case 'AMMCreate':
validateAMMCreate(tx)
break
case 'AMMVote':
validateAMMVote(tx)
break
@@ -242,6 +238,14 @@ export function validate(transaction: Record<string, unknown>): 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<string, unknown>): 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<string, unknown>): void {
validateTrustSet(tx)
break
case 'XChainAccountCreateCommit':
validateXChainAccountCreateCommit(tx)
break
case 'XChainAddAccountCreateAttestation':
validateXChainAddAccountCreateAttestation(tx)
break
@@ -358,10 +374,6 @@ export function validate(transaction: Record<string, unknown>): void {
validateXChainCreateClaimID(tx)
break
case 'XChainAccountCreateCommit':
validateXChainAccountCreateCommit(tx)
break
case 'XChainModifyBridge':
validateXChainModifyBridge(tx)
break

View File

@@ -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,
)
})

View File

@@ -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,
)
})

View File

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

View File

@@ -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',
)
})
})

View File

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

View File

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

View File

@@ -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])
}