diff --git a/packages/xahau-binary-codec/src/enums/definitions.json b/packages/xahau-binary-codec/src/enums/definitions.json index e051451e..c0c6bbf9 100644 --- a/packages/xahau-binary-codec/src/enums/definitions.json +++ b/packages/xahau-binary-codec/src/enums/definitions.json @@ -1218,6 +1218,16 @@ "type": "Hash256" } ], + [ + "ObjectID", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "BookDirectory", { @@ -1898,6 +1908,26 @@ "type": "Blob" } ], + [ + "RemarkValue", + { + "nth": 98, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], + [ + "RemarkName", + { + "nth": 99, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -2288,6 +2318,16 @@ "type": "STObject" } ], + [ + "Remark", + { + "nth": 97, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "GenesisMint", { @@ -2488,6 +2528,16 @@ "type": "STArray" } ], + [ + "Remarks", + { + "nth": 97, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "GenesisMints", { @@ -2632,6 +2682,7 @@ "tefPAST_IMPORT_SEQ": -178, "tefPAST_IMPORT_VL_SEQ": -177, "tefNONDIR_EMIT": -176, + "tefIMPORT_BLACKHOLED": -175, "terRETRY": -99, "terFUNDS_SPENT": -98, @@ -2724,6 +2775,8 @@ "tecXCHAIN_SELF_COMMIT": 185, "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 186, "tecINSUF_RESERVE_SELLER": 187, + "tecIMMUTABLE": 188, + "tecTOO_MANY_REMARKS": 189, "tecLAST_POSSIBLE_ENTRY": 255 }, "TRANSACTION_TYPES": { @@ -2761,6 +2814,7 @@ "URITokenBuy": 47, "URITokenCreateSellOffer": 48, "URITokenCancelSellOffer": 49, + "SetRemarks": 94, "Remit": 95, "GenesisMint": 96, "Import": 97, diff --git a/packages/xahau/src/models/transactions/index.ts b/packages/xahau/src/models/transactions/index.ts index 74172483..509f5aac 100644 --- a/packages/xahau/src/models/transactions/index.ts +++ b/packages/xahau/src/models/transactions/index.ts @@ -42,6 +42,12 @@ export { Remit } from './remit' export { SetHookFlagsInterface, SetHookFlags, SetHook } from './setHook' export { SetFee, SetFeePreAmendment, SetFeePostAmendment } from './setFee' export { SetRegularKey } from './setRegularKey' +export { + SetRemarks, + Remark, + RemarkFlags, + RemarkFlagsInterface, +} from './setRemarks' export { SignerListSet } from './signerListSet' export { TicketCreate } from './ticketCreate' export { TrustSetFlagsInterface, TrustSetFlags, TrustSet } from './trustSet' diff --git a/packages/xahau/src/models/transactions/setRemarks.ts b/packages/xahau/src/models/transactions/setRemarks.ts new file mode 100644 index 00000000..0c5fc99c --- /dev/null +++ b/packages/xahau/src/models/transactions/setRemarks.ts @@ -0,0 +1,90 @@ +import { ValidationError } from '../../errors' + +import { BaseTransaction, validateBaseTransaction } from './common' + +export enum RemarkFlags { + tfImmutable = 0x00000001, +} + +export interface RemarkFlagsInterface { + tfImmutable?: boolean +} + +export interface Remark { + Remark: { + RemarkName: string + RemarkValue?: string + Flags?: number | RemarkFlagsInterface + } +} + +/** + * A SetRemarks transaction assigns, changes, or removes the remarks associated with a object. + * + * @category Transaction Models + */ +export interface SetRemarks extends BaseTransaction { + TransactionType: 'SetRemarks' + ObjectID: string + Remarks: Remark[] +} + +const HEX_REGEX = /^[0-9A-Fa-f]{64}$/u +const MAX_REMARKS = 32 +const MAX_REMARK_NAME_LENGTH = 256 +const MAX_REMARK_VALUE_LENGTH = 256 + +/** + * Verify the form and type of a SetRemarks at runtime. + * + * @param tx - A SetRemarks Transaction. + * @throws When the SetRemarks is malformed. + */ +export function validateSetRemarks(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.ObjectID == null) { + throw new ValidationError('SetRemarks: ObjectID is required') + } + + if (typeof tx.ObjectID !== 'string' || !HEX_REGEX.test(tx.ObjectID)) { + throw new ValidationError( + 'SetRemarks: ObjectID must be a 256-bit (32-byte) hexadecimal value', + ) + } + + if (tx.Remarks == null) { + throw new ValidationError('SetRemarks: Remarks is required') + } + + if (!Array.isArray(tx.Remarks)) { + throw new ValidationError('SetRemarks: Remarks must be an array') + } + + if (tx.Remarks.length > MAX_REMARKS) { + throw new ValidationError( + `SetRemarks: maximum of ${MAX_REMARKS} remarks allowed in Remarks`, + ) + } + + for (const remark of tx.Remarks) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be a Remark + const remarkObject = remark as Remark + const { RemarkName, RemarkValue } = remarkObject.Remark + + if (RemarkName.length > MAX_REMARK_NAME_LENGTH * 2) { + throw new ValidationError( + `SetRemarks: maximum of ${MAX_REMARK_NAME_LENGTH} bytes allowed in RemarkName`, + ) + } + + if ( + RemarkValue != null && + RemarkValue.length > MAX_REMARK_VALUE_LENGTH * 2 + ) { + throw new ValidationError( + `SetRemarks: maximum of ${MAX_REMARK_VALUE_LENGTH} bytes allowed in RemarkValue`, + ) + } + } +} diff --git a/packages/xahau/src/models/transactions/transaction.ts b/packages/xahau/src/models/transactions/transaction.ts index 57cfe429..cf8a5235 100644 --- a/packages/xahau/src/models/transactions/transaction.ts +++ b/packages/xahau/src/models/transactions/transaction.ts @@ -38,6 +38,7 @@ import { Remit, validateRemit } from './remit' import { SetFee } from './setFee' import { SetHook, validateSetHook } from './setHook' import { SetRegularKey, validateSetRegularKey } from './setRegularKey' +import { SetRemarks, validateSetRemarks } from './setRemarks' import { SignerListSet, validateSignerListSet } from './signerListSet' import { TicketCreate, validateTicketCreate } from './ticketCreate' import { TrustSet, validateTrustSet } from './trustSet' @@ -80,6 +81,7 @@ export type SubmittableTransaction = | Remit | SetHook | SetRegularKey + | SetRemarks | SignerListSet | TicketCreate | TrustSet @@ -262,6 +264,10 @@ export function validate(transaction: Record): void { validateSetRegularKey(tx) break + case 'SetRemarks': + validateSetRemarks(tx) + break + case 'SignerListSet': validateSignerListSet(tx) break diff --git a/packages/xahau/src/models/utils/flags.ts b/packages/xahau/src/models/utils/flags.ts index 24a62337..9dbe795b 100644 --- a/packages/xahau/src/models/utils/flags.ts +++ b/packages/xahau/src/models/utils/flags.ts @@ -12,6 +12,11 @@ import { OfferCreateFlags } from '../transactions/offerCreate' import { PaymentFlags } from '../transactions/payment' import { PaymentChannelClaimFlags } from '../transactions/paymentChannelClaim' import { SetHookFlagsInterface, SetHookFlags } from '../transactions/setHook' +import { + RemarkFlagsInterface, + RemarkFlags, + Remark, +} from '../transactions/setRemarks' import type { Transaction } from '../transactions/transaction' import { TrustSetFlags } from '../transactions/trustSet' @@ -72,6 +77,14 @@ export function setTransactionFlagsToNumber(tx: Transaction): void { SetHookFlags, ) }) + } else if (tx.TransactionType === 'SetRemarks') { + tx.Remarks.forEach((remark: Remark) => { + remark.Remark.Flags = convertFlagsToNumber( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- idk + remark.Remark.Flags as RemarkFlagsInterface, + RemarkFlags, + ) + }) } tx.Flags = txToFlag[tx.TransactionType] diff --git a/packages/xahau/test/models/setRemarks.test.ts b/packages/xahau/test/models/setRemarks.test.ts new file mode 100644 index 00000000..332689d8 --- /dev/null +++ b/packages/xahau/test/models/setRemarks.test.ts @@ -0,0 +1,105 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateSetRemarks } from '../../src/models/transactions/setRemarks' + +/** + * SetRemarks Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('SetRemarks', function () { + let tx: any + + beforeEach(function () { + tx = { + TransactionType: 'SetRemarks', + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + Fee: '12', + ObjectID: + '0000000000000000000000000000000000000000000000000000000000000000', + Remarks: [ + { + Remark: { + RemarkName: 'DEADBEEF', + RemarkValue: 'DEADBEEF', + }, + }, + ], + } as any + }) + + it(`verifies valid SetRemarks`, function () { + assert.doesNotThrow(() => validateSetRemarks(tx)) + assert.doesNotThrow(() => validate(tx)) + + tx.Remarks[0].Remark.Flags = { tfImmutable: true } + assert.doesNotThrow(() => validateSetRemarks(tx)) + assert.doesNotThrow(() => validate(tx)) + + tx.Remarks = [{ Remark: { RemarkName: 'DEADBEEF' } }] + assert.doesNotThrow(() => validateSetRemarks(tx)) + assert.doesNotThrow(() => validate(tx)) + }) + + it(`throws w/ invalid ObjectID`, function () { + delete tx.ObjectID + let errorMessage = 'SetRemarks: ObjectID is required' + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + + tx.ObjectID = 'DEADBEEF' + errorMessage = + 'SetRemarks: ObjectID must be a 256-bit (32-byte) hexadecimal value' + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) + + it(`throws w/ invalid Remarks`, function () { + delete tx.Remarks + let errorMessage = 'SetRemarks: Remarks is required' + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + + tx.Remarks = 'DEABEEF' + errorMessage = 'SetRemarks: Remarks must be an array' + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + + tx.Remarks = Array(33) + .fill(null) + .map((_, idx) => ({ + Remark: { + RemarkName: `DEADBEEF${idx.toString(16)}`, + RemarkValue: `DEADBEEF${idx.toString(16)}`, + }, + })) + errorMessage = `SetRemarks: maximum of 32 remarks allowed in Remarks` + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + + tx.Remarks = [ + { + Remark: { + RemarkName: 'DEADBEEF'.repeat(256 / 4 + 1), + RemarkValue: 'DEADBEEF', + }, + }, + ] + errorMessage = `SetRemarks: maximum of 256 bytes allowed in RemarkName` + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + + tx.Remarks = [ + { + Remark: { + RemarkName: 'DEADBEEF', + RemarkValue: 'DEADBEEF'.repeat(256 / 4 + 1), + }, + }, + ] + errorMessage = `SetRemarks: maximum of 256 bytes allowed in RemarkValue` + assert.throws(() => validateSetRemarks(tx), ValidationError, errorMessage) + assert.throws(() => validate(tx), ValidationError, errorMessage) + }) +})