This commit is contained in:
tequ
2025-07-08 01:30:28 +09:00
committed by GitHub
parent 69e8b786ed
commit aa1ff0e32d
6 changed files with 274 additions and 0 deletions

View File

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

View File

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

View File

@@ -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<string, unknown>): 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`,
)
}
}
}

View File

@@ -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<string, unknown>): void {
validateSetRegularKey(tx)
break
case 'SetRemarks':
validateSetRemarks(tx)
break
case 'SignerListSet':
validateSignerListSet(tx)
break

View File

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

View File

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