add remit

This commit is contained in:
Denis Angell
2024-03-13 11:33:29 +01:00
parent 4aacb1160a
commit 63238773ea
8 changed files with 651 additions and 3 deletions

View File

@@ -111,6 +111,11 @@ const UNLReport = {
meta: require('./fixtures/unl-report-meta-binary.json'),
}
const Remit = {
tx: require('./fixtures/remit-tx.json'),
binary: require('./fixtures/remit-binary.json'),
}
function bytesListTest() {
const list = new BytesList()
.put(Buffer.from([0]))
@@ -291,6 +296,12 @@ function nfTokenTest() {
}
}
function RemitTest() {
test('can serialize Remit', () => {
expect(encode(Remit.tx)).toEqual(Remit.binary)
})
}
describe('Binary Serialization', function () {
describe('nestedObjectTests', nestedObjectTests)
describe('BytesList', bytesListTest)
@@ -304,4 +315,5 @@ describe('Binary Serialization', function () {
describe('TicketTest', ticketTest)
describe('NFToken', nfTokenTest)
describe('UNLReport', UNLReportTest)
describe('Remit', RemitTest)
})

View File

@@ -0,0 +1 @@
"12005F22000000002403EDEB4A2E00000001201B03EE5D3150116F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B68400000000000000C732102F9E33F16DF9507705EC954E3F94EB5F10D1FC4A354606DBE6297DBB1096FE65474473045022100E3FAE0EDEC3D6A8FF6D81BC9CF8288A61B7EEDE8071E90FF9314CB4621058D10022043545CF631706D700CEE65A1DB83EFDD185413808292D9D90F14D87D3DC2D8CB701A04DEADBEEF81147990EC5D1D8DF69E070A968D4B186986FDF06ED0831449FF0C73CA6AF9733DA805F76CA2C37776B7C46B806314757C4A9ED08284D61F3D8807280795F858BAB61DE05C220000000150156F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B7504DEADBEEFE1F05CE05B6140000000000003E8E1E05B61D4838D7EA4C68000000000000000000000000000555344000000000006B80F0F1D98AEDA846ED981F741C398FB2C4FD1E1F100136320AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE"

View File

@@ -0,0 +1,39 @@
{
"TransactionType": "Remit",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"DestinationTag": 1,
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 65953073,
"Sequence": 65923914,
"SigningPubKey": "02F9E33F16DF9507705EC954E3F94EB5F10D1FC4A354606DBE6297DBB1096FE654",
"TxnSignature": "3045022100E3FAE0EDEC3D6A8FF6D81BC9CF8288A61B7EEDE8071E90FF9314CB4621058D10022043545CF631706D700CEE65A1DB83EFDD185413808292D9D90F14D87D3DC2D8CB",
"Amounts": [
{
"AmountEntry": {
"Amount": "1000"
}
},
{
"AmountEntry": {
"Amount": {
"currency": "USD",
"issuer": "rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX",
"value": "1"
}
}
}
],
"MintURIToken": {
"URI": "DEADBEEF",
"Digest": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
"Flags": 1
},
"URITokenIDs": [
"AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE"
],
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
"Blob": "DEADBEEF",
"Inform": "rB5Ux4Lv2nRx6eeoAAsZmtctnBQ2LiACnk"
}

View File

@@ -17,6 +17,8 @@ export interface IssuedCurrencyAmount extends IssuedCurrency {
export type Amount = IssuedCurrencyAmount | string
export type AmountEntry = Amount
export interface Signer {
Signer: {
Account: string
@@ -205,3 +207,21 @@ export interface EmitDetails {
EmitHookHash: string
EmitParentTxnID: string
}
/**
* The object that describes the uritoken in MintURIToken.
*/
export interface MintURIToken {
/**
*
*/
URI: string
/**
*
*/
Digest?: string
/**
* The flags that are set on the uritoken.
*/
Flags?: number
}

View File

@@ -44,8 +44,9 @@ export {
} from './paymentChannelClaim'
export { PaymentChannelCreate } from './paymentChannelCreate'
export { PaymentChannelFund } from './paymentChannelFund'
export { SetRegularKey } from './setRegularKey'
export { Remit } from './remit'
export { SetHookFlagsInterface, SetHookFlags, SetHook } from './setHook'
export { SetRegularKey } from './setRegularKey'
export { SignerListSet } from './signerListSet'
export { TicketCreate } from './ticketCreate'
export { TrustSetFlagsInterface, TrustSetFlags, TrustSet } from './trustSet'

View File

@@ -0,0 +1,200 @@
import { ValidationError } from '../../errors'
import { AmountEntry, MintURIToken } from '../common'
import { isHex } from '../utils'
import { BaseTransaction, validateBaseTransaction } from './common'
const MAX_URI_LENGTH = 256
const DIGEST_LENGTH = 64
const MAX_ARRAY_LENGTH = 32
const MAX_BLOB_LENGTH = 1024
/**
* A Remit transaction represents a transfer of value from one account to
* another.
*
* @category Transaction Models
*/
export interface Remit extends BaseTransaction {
TransactionType: 'Remit'
/** The unique address of the account receiving the payment. */
Destination: string
/**
* Arbitrary tag that identifies the reason for the payment to the
* destination, or a hosted recipient to pay.
*/
DestinationTag?: number
/**
*
*/
Amounts?: AmountEntry[]
/**
*
*/
MintURIToken?: MintURIToken
/**
* Arbitrary 256-bit hash representing a specific reason or identifier for
* this payment.
*/
InvoiceID?: string
/**
* Hex value representing a VL Blob.
*/
Blob?: string
/** The unique address of the account to inform */
Inform?: string
}
/**
* Verify the form and type of a Remit at runtime.
*
* @param tx - A Remit Transaction.
* @throws When the Remit is malformed.
*/
// eslint-disable-next-line complexity -- ignore
export function validateRemit(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
if (tx.Amounts !== undefined) {
checkAmounts(tx)
}
if (tx.URITokenIDs !== undefined) {
checkURITokenIDs(tx)
}
if (tx.Destination === tx.Account) {
throw new ValidationError(
'Remit: Destination must not be equal to the account',
)
}
if (tx.DestinationTag != null && typeof tx.DestinationTag !== 'number') {
throw new ValidationError('Remit: DestinationTag must be a number')
}
if (tx.Inform === tx.Account || tx.inform === tx.Destination) {
throw new ValidationError(
'Remit: Inform must not be equal to the account or destination',
)
}
if (tx.MintURIToken !== undefined) {
checkMintURIToken(tx)
}
if (tx.Blob !== undefined && typeof tx.Blob !== 'string') {
throw new ValidationError('Remit: Blob must be a string')
}
if (tx.Blob !== undefined && typeof tx.Blob === 'string') {
if (!isHex(tx.Blob)) {
throw new ValidationError('Remit: Blob must be a hex string')
}
if (tx.Blob.length > MAX_BLOB_LENGTH) {
throw new ValidationError('Remit: max size Blob')
}
}
}
function checkAmounts(tx: Record<string, unknown>): void {
if (!Array.isArray(tx.Amounts)) {
throw new ValidationError('Remit: Amounts must be an array')
}
if (tx.Amounts.length < 1) {
throw new ValidationError('Remit: empty field Amounts')
}
if (tx.Amounts.length > MAX_ARRAY_LENGTH) {
throw new ValidationError('Remit: max field Amounts')
}
const seen = new Set<string>()
let seenXrp = false
for (const amount of tx.Amounts) {
if (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- ignore
amount.AmountEntry === undefined ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- ignore
typeof amount.AmountEntry !== 'object'
) {
throw new ValidationError('Remit: invalid Amounts.AmountEntry')
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- ignore
if (typeof amount.AmountEntry.Amount === 'string') {
// eslint-disable-next-line max-depth -- ignore
if (seenXrp) {
throw new ValidationError(
'Remit: Duplicate Native amounts are not allowed',
)
}
seenXrp = true
} else {
// eslint-disable-next-line max-len -- ignore
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access -- ignore
const amountKey = `${amount.AmountEntry.Amount.currency}:${amount.AmountEntry.Amount.issuer}`
// eslint-disable-next-line max-depth -- ingore
if (seen.has(amountKey)) {
throw new ValidationError('Remit: Duplicate amounts are not allowed')
}
seen.add(amountKey)
}
}
}
function checkURITokenIDs(tx: Record<string, unknown>): void {
if (!Array.isArray(tx.URITokenIDs)) {
throw new ValidationError('Remit: invalid field URITokenIDs')
}
if (tx.URITokenIDs.length < 1) {
throw new ValidationError('Remit: empty field URITokenIDs')
}
if (tx.URITokenIDs.length > MAX_ARRAY_LENGTH) {
throw new ValidationError('Remit: max field URITokenIDs')
}
const seen = new Set<string>()
for (const token of tx.URITokenIDs) {
if (typeof token !== 'string' || !isHex(token)) {
throw new ValidationError('Remit: URITokenID must be a hex string')
}
if (token.length !== DIGEST_LENGTH) {
throw new ValidationError(
`Remit: URITokenID must be exactly ${DIGEST_LENGTH} characters`,
)
}
if (seen.has(token)) {
throw new ValidationError('Remit: Duplicate URITokens are not allowed')
}
seen.add(token)
}
}
// eslint-disable-next-line complexity -- ignore
function checkMintURIToken(tx: Record<string, unknown>): void {
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
}
if (!isRecord(tx.MintURIToken)) {
throw new ValidationError('Remit: invalid MintURIToken')
}
if (tx.MintURIToken.URI === undefined) {
throw new ValidationError('Remit: missing field MintURIToken.URI')
}
if (typeof tx.MintURIToken.URI !== 'string' || !isHex(tx.MintURIToken.URI)) {
throw new ValidationError('Remit: MintURIToken.URI must be a hex string')
}
if (tx.MintURIToken.URI.length > MAX_URI_LENGTH) {
throw new ValidationError(
`Remit: URI must be less than ${MAX_URI_LENGTH} characters`,
)
}
if (
tx.MintURIToken.Digest !== undefined &&
typeof tx.MintURIToken.Digest !== 'string'
) {
throw new ValidationError(`Remit: MintURIToken.Digest must be a string`)
}
if (
tx.MintURIToken.Digest !== undefined &&
!isHex(tx.MintURIToken.Digest) &&
tx.MintURIToken.Digest.length !== DIGEST_LENGTH
) {
throw new ValidationError(
`Remit: Digest must be exactly ${DIGEST_LENGTH} characters`,
)
}
}

View File

@@ -46,6 +46,7 @@ import {
PaymentChannelFund,
validatePaymentChannelFund,
} from './paymentChannelFund'
import { Remit, validateRemit } from './remit'
import { SetHook, validateSetHook } from './setHook'
import { SetRegularKey, validateSetRegularKey } from './setRegularKey'
import { SignerListSet, validateSignerListSet } from './signerListSet'
@@ -90,6 +91,7 @@ export type Transaction =
| PaymentChannelClaim
| PaymentChannelCreate
| PaymentChannelFund
| Remit
| SetHook
| SetRegularKey
| SignerListSet
@@ -220,14 +222,18 @@ export function validate(transaction: Record<string, unknown>): void {
validatePaymentChannelFund(tx)
break
case 'SetRegularKey':
validateSetRegularKey(tx)
case 'Remit':
validateRemit(tx)
break
case 'SetHook':
validateSetHook(tx)
break
case 'SetRegularKey':
validateSetRegularKey(tx)
break
case 'SignerListSet':
validateSignerListSet(tx)
break

View File

@@ -0,0 +1,369 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
import { validateRemit } from '../../src/models/transactions/remit'
/**
* Remit Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
// eslint-disable-next-line max-statements -- ignore
describe('Remit', function () {
let remit
beforeEach(function () {
remit = {
TransactionType: 'Remit',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
DestinationTag: 1,
Fee: '12',
Flags: 0,
LastLedgerSequence: 65953073,
Sequence: 65923914,
SigningPubKey:
'02F9E33F16DF9507705EC954E3F94EB5F10D1FC4A354606DBE6297DBB1096FE654',
TxnSignature:
'3045022100E3FAE0EDEC3D6A8FF6D81BC9CF8288A61B7EEDE8071E90FF9314CB4621058D10022043545CF631706D700CEE65A1DB83EFDD185413808292D9D90F14D87D3DC2D8CB',
Amounts: [
{ AmountEntry: { Amount: '1000' } },
{
AmountEntry: {
Amount: {
currency: 'USD',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '1',
},
},
},
],
MintURIToken: {
URI: 'DEADBEEF',
Digest:
'6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B',
Flags: 1,
},
URITokenIDs: [
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE',
],
InvoiceID:
'6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B',
Blob: 'DEADBEEF',
Inform: 'rB5Ux4Lv2nRx6eeoAAsZmtctnBQ2LiACnk',
} as any
})
it(`verifies valid Remit`, function () {
assert.doesNotThrow(() => validateRemit(remit))
assert.doesNotThrow(() => validate(remit))
})
it(`throws w/ Bad Amounts`, function () {
remit.Amounts = {}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Amounts must be an array',
)
})
it(`throws w/ Empty Amounts`, function () {
remit.Amounts = []
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: empty field Amounts',
)
})
it(`throws w/ Max Amounts`, function () {
remit.Amounts = [
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: max field Amounts',
)
})
it(`throws w/ Duplicate native amounts`, function () {
remit.Amounts = [
{ AmountEntry: { Amount: '1000' } },
{ AmountEntry: { Amount: '1000' } },
]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Duplicate Native amounts are not allowed',
)
})
it(`throws w/ Duplicate amounts`, function () {
remit.Amounts = [
{
AmountEntry: {
Amount: {
currency: 'USD',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '1',
},
},
},
{
AmountEntry: {
Amount: {
currency: 'USD',
issuer: 'rcXY84C4g14iFp6taFXjjQGVeHqSCh9RX',
value: '1',
},
},
},
]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Duplicate amounts are not allowed',
)
})
it(`throws w/ Bad URITokenIDs`, function () {
remit.URITokenIDs = {}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: invalid field URITokenIDs',
)
})
it(`throws w/ Empty URITokenIDs`, function () {
remit.URITokenIDs = []
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: empty field URITokenIDs',
)
})
it(`throws w/ Empty URITokenIDs`, function () {
remit.URITokenIDs = [
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
'DEADBEEF',
]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: max field URITokenIDs',
)
})
it(`throws w/ Invalid URITokenID`, function () {
remit.URITokenIDs = [1]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: URITokenID must be a hex string',
)
})
it(`throws w/ Invalid URITokenID`, function () {
remit.URITokenIDs = ['ZZ11']
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: URITokenID must be a hex string',
)
})
it(`throws w/ Duplicate URITokenIDs`, function () {
remit.URITokenIDs = ['DEADBEEF']
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: URITokenID must be exactly 64 characters',
)
})
it(`throws w/ Duplicate URITokenIDs`, function () {
remit.URITokenIDs = [
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE',
'AED08CC1F50DD5F23A1948AF86153A3F3B7593E5EC77D65A02BB1B29E05AB6AE',
]
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Duplicate URITokens are not allowed',
)
})
// it(`throws w/ Bad MintURIToken`, function () {
// remit.MintURIToken = []
// assert.throws(
// () => validateRemit(remit),
// ValidationError,
// 'Remit: invalid MintURIToken',
// )
// })
it(`throws w/ Missing MintURIToken.URI`, function () {
remit.MintURIToken = {}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: missing field MintURIToken.URI',
)
})
it(`throws w/ Bad MintURIToken.URI`, function () {
remit.MintURIToken = {
URI: 1,
}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: MintURIToken.URI must be a hex string',
)
})
it(`throws w/ Bad MintURIToken.URI`, function () {
remit.MintURIToken = {
URI: 'ZZ11',
}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: MintURIToken.URI must be a hex string',
)
})
it(`throws w/ Bad MintURIToken Less than 1`, function () {
remit.MintURIToken = {
URI: '',
}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: MintURIToken.URI must be a hex string',
)
})
it(`throws w/ Bad MintURIToken.Digest`, function () {
remit.MintURIToken = {
URI: 'DEADBEEF',
Digest: 1,
}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: MintURIToken.Digest must be a string',
)
})
it(`throws w/ Bad MintURIToken.Digest`, function () {
remit.MintURIToken = {
URI: 'DEADBEEF',
Digest: 'ZZ11',
}
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Digest must be exactly 64 characters',
)
})
it(`throws w/ Bad Destination`, function () {
remit.Destination = 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo'
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Destination must not be equal to the account',
)
})
it(`throws w/ Bad Destination Tag`, function () {
remit.DestinationTag = '1'
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: DestinationTag must be a number',
)
})
it(`throws w/ Bad Inform`, function () {
remit.Inform = 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo'
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Inform must not be equal to the account or destination',
)
})
it(`throws w/ Bad Blob Type`, function () {
remit.Blob = 1
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Blob must be a string',
)
})
it(`throws w/ Bad Blob Not Hex`, function () {
remit.Blob = 'ZZ11'
assert.throws(
() => validateRemit(remit),
ValidationError,
'Remit: Blob must be a hex string',
)
})
// it(`throws w/ Bad Blob Max Size`, function () {
// remit.Blob = ''
// assert.throws(
// () => validateRemit(remit),
// ValidationError,
// 'Remit: Blob must be a hex string',
// )
// })
})