MPT Support for library and binary codec
This commit is contained in:
Shawn Xie
2024-12-11 13:38:13 -08:00
committed by GitHub
parent e3188b83ed
commit b04efe8c9e
47 changed files with 3042 additions and 1067 deletions

View File

@@ -178,3 +178,12 @@ PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
# 2.3.0-rc1 Amendments
fixAMMv1_1
Credentials
NFTokenMintOffer
MPTokensV1
fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.2.0-b3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
on:
push:
@@ -108,7 +108,7 @@ jobs:
- name: Run docker in background
run: |
docker run --detach --rm --name rippled-service -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_nfo || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a"
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
@@ -165,7 +165,7 @@ jobs:
- name: Run docker in background
run: |
docker run --detach --rm --name rippled-service -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_nfo || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a"
- name: Setup npm version 9
run: |

View File

@@ -2,6 +2,9 @@
## Unreleased
### Added
* Support for the Multi-Purpose Token amendment (XLS-33)
## 2.1.0 (2024-06-03)
### Added

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { JsonObject, SerializedType } from './serialized-type'
import BigNumber from 'bignumber.js'
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
import { readUInt32BE, writeUInt32BE } from '../utils'
import { Hash192 } from './hash-192'
/**
* Constants for validating amounts
@@ -16,6 +17,7 @@ const MAX_IOU_PRECISION = 16
const MAX_DROPS = new BigNumber('1e17')
const MIN_XRP = new BigNumber('1e-6')
const mask = BigInt(0x00000000ffffffff)
const mptMask = BigInt(0x8000000000000000)
/**
* BigNumber configuration for Amount IOUs
@@ -27,20 +29,28 @@ BigNumber.config({
],
})
/**
* Interface for JSON objects that represent amounts
*/
interface AmountObject extends JsonObject {
interface AmountObjectIOU extends JsonObject {
value: string
currency: string
issuer: string
}
interface AmountObjectMPT extends JsonObject {
value: string
mpt_issuance_id: string
}
/**
* Type guard for AmountObject
* Interface for JSON objects that represent amounts
*/
function isAmountObject(arg): arg is AmountObject {
type AmountObject = AmountObjectIOU | AmountObjectMPT
/**
* Type guard for AmountObjectIOU
*/
function isAmountObjectIOU(arg): arg is AmountObjectIOU {
const keys = Object.keys(arg).sort()
return (
keys.length === 3 &&
keys[0] === 'currency' &&
@@ -49,6 +59,17 @@ function isAmountObject(arg): arg is AmountObject {
)
}
/**
* Type guard for AmountObjectMPT
*/
function isAmountObjectMPT(arg): arg is AmountObjectMPT {
const keys = Object.keys(arg).sort()
return (
keys.length === 2 && keys[0] === 'mpt_issuance_id' && keys[1] === 'value'
)
}
/**
* Class for serializing/Deserializing Amounts
*/
@@ -60,7 +81,7 @@ class Amount extends SerializedType {
}
/**
* Construct an amount from an IOU or string amount
* Construct an amount from an IOU, MPT or string amount
*
* @param value An Amount, object representing an IOU, or a string
* representing an integer amount
@@ -88,7 +109,7 @@ class Amount extends SerializedType {
return new Amount(amount)
}
if (isAmountObject(value)) {
if (isAmountObjectIOU(value)) {
const number = new BigNumber(value.value)
Amount.assertIouIsValid(number)
@@ -124,6 +145,24 @@ class Amount extends SerializedType {
return new Amount(concat([amount, currency, issuer]))
}
if (isAmountObjectMPT(value)) {
Amount.assertMptIsValid(value.value)
let leadingByte = new Uint8Array(1)
leadingByte[0] |= 0x60
const num = BigInt(value.value)
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
writeUInt32BE(intBuf[0], Number(num >> BigInt(32)), 0)
writeUInt32BE(intBuf[1], Number(num & BigInt(mask)), 0)
amount = concat(intBuf)
const mptIssuanceID = Hash192.from(value.mpt_issuance_id).toBytes()
return new Amount(concat([leadingByte, amount, mptIssuanceID]))
}
throw new Error('Invalid type to construct an Amount')
}
@@ -134,8 +173,12 @@ class Amount extends SerializedType {
* @returns An Amount object
*/
static fromParser(parser: BinaryParser): Amount {
const isXRP = parser.peek() & 0x80
const numBytes = isXRP ? 48 : 8
const isIOU = parser.peek() & 0x80
if (isIOU) return new Amount(parser.read(48))
// the amount can be either MPT or XRP at this point
const isMPT = parser.peek() & 0x20
const numBytes = isMPT ? 33 : 8
return new Amount(parser.read(numBytes))
}
@@ -156,7 +199,9 @@ class Amount extends SerializedType {
const num = (msb << BigInt(32)) | lsb
return `${sign}${num.toString()}`
} else {
}
if (this.isIOU()) {
const parser = new BinaryParser(this.toString())
const mantissa = parser.read(8)
const currency = Currency.fromParser(parser) as Currency
@@ -182,6 +227,27 @@ class Amount extends SerializedType {
issuer: issuer.toJSON(),
}
}
if (this.isMPT()) {
const parser = new BinaryParser(this.toString())
const leadingByte = parser.read(1)
const amount = parser.read(8)
const mptID = Hash192.fromParser(parser) as Hash192
const isPositive = leadingByte[0] & 0x40
const sign = isPositive ? '' : '-'
const msb = BigInt(readUInt32BE(amount.slice(0, 4), 0))
const lsb = BigInt(readUInt32BE(amount.slice(4), 0))
const num = (msb << BigInt(32)) | lsb
return {
value: `${sign}${num.toString()}`,
mpt_issuance_id: mptID.toString(),
}
}
throw new Error('Invalid amount to construct JSON')
}
/**
@@ -224,6 +290,29 @@ class Amount extends SerializedType {
}
}
/**
* Validate MPT.value amount
*
* @param decimal BigNumber object representing MPT.value
* @returns void, but will throw if invalid amount
*/
private static assertMptIsValid(amount: string): void {
if (amount.indexOf('.') !== -1) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
const decimal = new BigNumber(amount)
if (!decimal.isZero()) {
if (decimal < BigNumber(0)) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
if (Number(BigInt(amount) & BigInt(mptMask)) != 0) {
throw new Error(`${amount.toString()} is an illegal amount`)
}
}
}
/**
* Ensure that the value after being multiplied by the exponent does not
* contain a decimal.
@@ -248,7 +337,25 @@ class Amount extends SerializedType {
* @returns true if Native (XRP)
*/
private isNative(): boolean {
return (this.bytes[0] & 0x80) === 0
return (this.bytes[0] & 0x80) === 0 && (this.bytes[0] & 0x20) === 0
}
/**
* Test if this amount is in units of MPT
*
* @returns true if MPT
*/
private isMPT(): boolean {
return (this.bytes[0] & 0x80) === 0 && (this.bytes[0] & 0x20) !== 0
}
/**
* Test if this amount is in units of IOU
*
* @returns true if IOU
*/
private isIOU(): boolean {
return (this.bytes[0] & 0x80) !== 0
}
}

View File

@@ -0,0 +1,19 @@
import { Hash } from './hash'
/**
* Hash with a width of 192 bits
*/
class Hash192 extends Hash {
static readonly width = 24
static readonly ZERO_192: Hash192 = new Hash192(new Uint8Array(Hash192.width))
constructor(bytes?: Uint8Array) {
if (bytes && bytes.byteLength === 0) {
bytes = Hash192.ZERO_192.bytes
}
super(bytes ?? Hash192.ZERO_192.bytes)
}
}
export { Hash192 }

View File

@@ -4,6 +4,7 @@ import { Blob } from './blob'
import { Currency } from './currency'
import { Hash128 } from './hash-128'
import { Hash160 } from './hash-160'
import { Hash192 } from './hash-192'
import { Hash256 } from './hash-256'
import { Issue } from './issue'
import { PathSet } from './path-set'
@@ -25,6 +26,7 @@ const coreTypes: Record<string, typeof SerializedType> = {
Currency,
Hash128,
Hash160,
Hash192,
Hash256,
Issue,
PathSet,
@@ -51,6 +53,7 @@ export {
Currency,
Hash128,
Hash160,
Hash192,
Hash256,
PathSet,
STArray,

View File

@@ -67,7 +67,7 @@ class SerializedType {
* Can be customized for sidechains and amendments.
* @returns any type, if not overloaded returns hexString representation of bytes
*/
toJSON(_definitions?: XrplDefinitionsBase): JSON {
toJSON(_definitions?: XrplDefinitionsBase, _fieldName?: string): JSON {
return this.toHex()
}

View File

@@ -10,6 +10,7 @@ import { BinaryParser } from '../serdes/binary-parser'
import { BinarySerializer, BytesList } from '../serdes/binary-serializer'
import { STArray } from './st-array'
import { UInt64 } from './uint-64'
const OBJECT_END_MARKER_BYTE = Uint8Array.from([0xe1])
const OBJECT_END_MARKER = 'ObjectEndMarker'
@@ -137,6 +138,8 @@ class STObject extends SerializedType {
? this.from(xAddressDecoded[field.name], undefined, definitions)
: field.type.name === 'STArray'
? STArray.from(xAddressDecoded[field.name], definitions)
: field.type.name === 'UInt64'
? UInt64.from(xAddressDecoded[field.name], field.name)
: field.associatedType.from(xAddressDecoded[field.name])
if (associatedValue == undefined) {
@@ -182,7 +185,7 @@ class STObject extends SerializedType {
accumulator[field.name] = objectParser
.readFieldValue(field)
.toJSON(definitions)
.toJSON(definitions, field.name)
}
return accumulator

View File

@@ -2,10 +2,20 @@ import { UInt } from './uint'
import { BinaryParser } from '../serdes/binary-parser'
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
import { readUInt32BE, writeUInt32BE } from '../utils'
import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums'
const HEX_REGEX = /^[a-fA-F0-9]{1,16}$/
const BASE10_REGEX = /^[0-9]{1,20}$/
const mask = BigInt(0x00000000ffffffff)
function useBase10(fieldName: string): boolean {
return (
fieldName === 'MaximumAmount' ||
fieldName === 'OutstandingAmount' ||
fieldName === 'MPTAmount'
)
}
/**
* Derived UInt class for serializing/deserializing 64 bit UInt
*/
@@ -29,7 +39,10 @@ class UInt64 extends UInt {
* @param val A UInt64, hex-string, bigInt, or number
* @returns A UInt64 object
*/
static from<T extends UInt64 | string | bigint | number>(val: T): UInt64 {
static from<T extends UInt64 | string | bigint | number>(
val: T,
fieldName = '',
): UInt64 {
if (val instanceof UInt64) {
return val
}
@@ -51,11 +64,18 @@ class UInt64 extends UInt {
}
if (typeof val === 'string') {
if (!HEX_REGEX.test(val)) {
if (useBase10(fieldName)) {
if (!BASE10_REGEX.test(val)) {
throw new Error(`${fieldName} ${val} is not a valid base 10 string`)
}
val = BigInt(val).toString(16) as T
}
if (typeof val === 'string' && !HEX_REGEX.test(val)) {
throw new Error(`${val} is not a valid hex-string`)
}
const strBuf = val.padStart(16, '0')
const strBuf = (val as string).padStart(16, '0')
buf = hexToBytes(strBuf)
return new UInt64(buf)
}
@@ -76,8 +96,16 @@ class UInt64 extends UInt {
*
* @returns a hex-string
*/
toJSON(): string {
return bytesToHex(this.bytes)
toJSON(
_definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
fieldName = '',
): string {
const hexString = bytesToHex(this.bytes)
if (useBase10(fieldName)) {
return BigInt('0x' + hexString).toString(10)
}
return hexString
}
/**

View File

@@ -1,5 +1,7 @@
import { coreTypes } from '../src/types'
import fixtures from './fixtures/data-driven-tests.json'
import { makeParser } from '../src/binary'
const { Amount } = coreTypes
function amountErrorTests() {
@@ -25,6 +27,16 @@ describe('Amount', function () {
it('can be parsed from', function () {
expect(Amount.from('1000000') instanceof Amount).toBe(true)
expect(Amount.from('1000000').toJSON()).toEqual('1000000')
// it not valid to have negative XRP. But we test it anyways
// to ensure logic correctness for toJSON of the Amount class
{
const parser = makeParser('0000000000000001')
const value = parser.readType(Amount)
const json = value.toJSON()
expect(json).toEqual('-1')
}
const fixture = {
value: '1',
issuer: '0000000000000000000000000000000000000000',
@@ -38,5 +50,35 @@ describe('Amount', function () {
}
expect(amt.toJSON()).toEqual(rewritten)
})
it('can be parsed from MPT', function () {
let fixture = {
value: '100',
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
}
let amt = Amount.from(fixture)
expect(amt.toJSON()).toEqual(fixture)
fixture = {
value: '9223372036854775807',
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
}
amt = Amount.from(fixture)
expect(amt.toJSON()).toEqual(fixture)
// it not valid to have negative MPT. But we test it anyways
// to ensure logic correctness for toJSON of the Amount class
{
const parser = makeParser(
'20000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF',
)
const value = parser.readType(Amount)
const json = value.toJSON()
expect(json).toEqual({
mpt_issuance_id: '00002403C84A0A28E0190E208E982C352BBD5006600555CF',
value: '-100',
})
}
})
amountErrorTests()
})

View File

@@ -22,6 +22,7 @@ function assertEqualAmountJSON(actual, expected) {
}
expect(actual.currency).toEqual(expected.currency)
expect(actual.issuer).toEqual(expected.issuer)
expect(actual.mpt_issuance_id).toEqual(expected.mpt_issuance_id)
expect(
actual.value === expected.value ||
new BigNumber(actual.value).eq(new BigNumber(expected.value)),
@@ -207,12 +208,12 @@ function amountParsingTests() {
return
}
const parser = makeParser(f.expected_hex)
const testName = `values_tests[${i}] parses ${f.expected_hex.slice(
const hexToJsonTestName = `values_tests[${i}] parses ${f.expected_hex.slice(
0,
16,
)}...
as ${JSON.stringify(f.test_json)}`
it(testName, () => {
it(hexToJsonTestName, () => {
const value = parser.readType(Amount)
// May not actually be in canonical form. The fixtures are to be used
// also for json -> binary;
@@ -223,6 +224,15 @@ function amountParsingTests() {
expect((exponent.e ?? 0) - 15).toEqual(f?.exponent)
}
})
const jsonToHexTestName = `values_tests[${i}] parses ${JSON.stringify(
f.test_json,
)}...
as ${f.expected_hex.slice(0, 16)}`
it(jsonToHexTestName, () => {
const amt = Amount.from(f.test_json)
expect(amt.toHex()).toEqual(f.expected_hex)
})
})
}

View File

@@ -2499,7 +2499,7 @@
"type_id": 6,
"is_native": true,
"type": "Amount",
"expected_hex": "0000000000000001",
"error": "Value is negative",
"is_negative": true
},
{
@@ -2914,6 +2914,170 @@
"type": "Amount",
"error": "10000000000000000000 absolute XRP is bigger than max native value 100000000000.0",
"is_negative": true
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "9223372036854775808"
},
"type": "Amount",
"error": "Value is too large"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "18446744073709551615"
},
"type": "Amount",
"error": "Value is too large"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "-1"
},
"type": "Amount",
"error": "Value is negative"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10.1"
},
"type": "Amount",
"error": "Value has decimal point"
},
{
"test_json": {
"mpt_issuance_id": "10",
"value": "10"
},
"type": "Amount",
"error": "mpt_issuance_id has invalid hash length"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10",
"issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji"
},
"type": "Amount",
"error": "Issuer not valid for MPT"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "10",
"currency": "USD"
},
"type": "Amount",
"error": "Currency not valid for MPT"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "a"
},
"type": "Amount",
"error": "Value has incorrect hex format"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xy"
},
"type": "Amount",
"error": "Value has bad hex character"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "/"
},
"type": "Amount",
"error": "Value has bad character"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0x8000000000000000"
},
"type": "Amount",
"error": "Hex value out of range"
},
{
"test_json": {
"mpt_issuance_id": "00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xFFFFFFFFFFFFFFFF"
},
"type": "Amount",
"error": "Hex value out of range"
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "9223372036854775807"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "-0"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000000002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "100"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000006400002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0xa"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "60000000000000000A00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
},
{
"test_json": {
"mpt_issuance_id":"00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"value": "0x7FFFFFFFFFFFFFFF"
},
"type_id": 6,
"is_native": false,
"type": "Amount",
"expected_hex": "607FFFFFFFFFFFFFFF00002403C84A0A28E0190E208E982C352BBD5006600555CF",
"is_negative": false
}
]
}

View File

@@ -1,4 +1,11 @@
import { Hash128, Hash160, Hash256, AccountID, Currency } from '../src/types'
import {
Hash128,
Hash160,
Hash192,
Hash256,
AccountID,
Currency,
} from '../src/types'
describe('Hash128', function () {
it('has a static width member', function () {
@@ -51,6 +58,33 @@ describe('Hash160', function () {
})
})
describe('Hash192', function () {
it('has a static width member', function () {
expect(Hash192.width).toBe(24)
})
it('has a ZERO_192 member', function () {
expect(Hash192.ZERO_192.toJSON()).toBe(
'000000000000000000000000000000000000000000000000',
)
})
it('can be compared against another', function () {
const h1 = Hash192.from('100000000000000000000000000000000000000000000000')
const h2 = Hash192.from('200000000000000000000000000000000000000000000000')
const h3 = Hash192.from('000000000000000000000000000000000000000000000003')
expect(h1.lt(h2)).toBe(true)
expect(h3.lt(h2)).toBe(true)
})
it('throws when constructed from invalid hash length', () => {
expect(() =>
Hash192.from('10000000000000000000000000000000000000000000000'),
).toThrow(new Error('Invalid Hash length 23'))
expect(() =>
Hash192.from('10000000000000000000000000000000000000000000000000'),
).toThrow(new Error('Invalid Hash length 25'))
})
})
describe('Hash256', function () {
it('has a static width member', function () {
expect(Hash256.width).toBe(32)

View File

@@ -1,5 +1,5 @@
import { UInt8, UInt64 } from '../src/types'
import { encode } from '../src'
import { encode, decode } from '../src'
const binary =
'11007222000300003700000000000000003800000000000000006280000000000000000000000000000000000000005553440000000000000000000000000000000000000000000000000166D5438D7EA4C680000000000000000000000000005553440000000000AE123A8556F3CF91154711376AFB0F894F832B3D67D5438D7EA4C680000000000000000000000000005553440000000000F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F'
@@ -96,6 +96,40 @@ const jsonEntry2 = {
index: '0000041EFD027808D3F78C8352F97E324CB816318E00B977C74ECDDC7CD975B2',
}
const mptIssuanceEntryBinary =
'11007E220000006224000002DF25000002E434000000000000000030187FFFFFFFFFFFFFFF30190000000000000064552E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE701EC1EC7B226E616D65223A2255532054726561737572792042696C6C20546F6B656E222C2273796D626F6C223A225553544254222C22646563696D616C73223A322C22746F74616C537570706C79223A313030303030302C22697373756572223A225553205472656173757279222C22697373756544617465223A22323032342D30332D3235222C226D6174757269747944617465223A22323032352D30332D3235222C226661636556616C7565223A2231303030222C22696E74657265737452617465223A22322E35222C22696E7465726573744672657175656E6379223A22517561727465726C79222C22636F6C6C61746572616C223A22555320476F7665726E6D656E74222C226A7572697364696374696F6E223A22556E6974656420537461746573222C22726567756C61746F7279436F6D706C69616E6365223A2253454320526567756C6174696F6E73222C22736563757269747954797065223A2254726561737572792042696C6C222C2265787465726E616C5F75726C223A2268747470733A2F2F6578616D706C652E636F6D2F742D62696C6C2D746F6B656E2D6D657461646174612E6A736F6E227D8414A4D893CFBC4DC6AE877EB585F90A3B47528B958D051003'
const mptIssuanceEntryJson = {
AssetScale: 3,
Flags: 98,
Issuer: 'rGpdGXDV2RFPeLEfWS9RFo5Nh9cpVDToZa',
LedgerEntryType: 'MPTokenIssuance',
MPTokenMetadata:
'7B226E616D65223A2255532054726561737572792042696C6C20546F6B656E222C2273796D626F6C223A225553544254222C22646563696D616C73223A322C22746F74616C537570706C79223A313030303030302C22697373756572223A225553205472656173757279222C22697373756544617465223A22323032342D30332D3235222C226D6174757269747944617465223A22323032352D30332D3235222C226661636556616C7565223A2231303030222C22696E74657265737452617465223A22322E35222C22696E7465726573744672657175656E6379223A22517561727465726C79222C22636F6C6C61746572616C223A22555320476F7665726E6D656E74222C226A7572697364696374696F6E223A22556E6974656420537461746573222C22726567756C61746F7279436F6D706C69616E6365223A2253454320526567756C6174696F6E73222C22736563757269747954797065223A2254726561737572792042696C6C222C2265787465726E616C5F75726C223A2268747470733A2F2F6578616D706C652E636F6D2F742D62696C6C2D746F6B656E2D6D657461646174612E6A736F6E227D',
MaximumAmount: '9223372036854775807',
OutstandingAmount: '100',
OwnerNode: '0000000000000000',
PreviousTxnID:
'2E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE',
PreviousTxnLgrSeq: 740,
Sequence: 735,
}
const mptokenEntryJson = {
Account: 'raDQsd1s8rqGjL476g59a9vVNi1rSwrC44',
Flags: 0,
LedgerEntryType: 'MPToken',
MPTAmount: '100',
MPTokenIssuanceID: '000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC',
OwnerNode: '0000000000000000',
PreviousTxnID:
'222EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C8',
PreviousTxnLgrSeq: 741,
}
const mptokenEntryBinary =
'11007F220000000025000002E5340000000000000000301A000000000000006455222EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C881143930DB9A74C26D96CB58ADFFD7E8BB78BCFE62340115000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC'
it('compareToTests[0]', () => {
expect(UInt8.from(124).compareTo(UInt64.from(124))).toBe(0)
})
@@ -144,3 +178,20 @@ it('valueOf tests', () => {
expect(val.valueOf() | 0x2).toBe(3)
})
it('UInt64 is parsed as base 10 for MPT amounts', () => {
expect(encode(mptIssuanceEntryJson)).toEqual(mptIssuanceEntryBinary)
expect(decode(mptIssuanceEntryBinary)).toEqual(mptIssuanceEntryJson)
expect(encode(mptokenEntryJson)).toEqual(mptokenEntryBinary)
expect(decode(mptokenEntryBinary)).toEqual(mptokenEntryJson)
const decodedIssuance = decode(mptIssuanceEntryBinary)
expect(typeof decodedIssuance.MaximumAmount).toBe('string')
expect(decodedIssuance.MaximumAmount).toBe('9223372036854775807')
expect(decodedIssuance.OutstandingAmount).toBe('100')
const decodedToken = decode(mptokenEntryBinary)
expect(typeof decodedToken.MPTAmount).toBe('string')
expect(decodedToken.MPTAmount).toBe('100')
})

View File

@@ -6,6 +6,8 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
### Added
* parseTransactionFlags as a utility function in the xrpl package to streamline transactions flags-to-map conversion
* Added new MPT transaction definitions (XLS-33)
* New `MPTAmount` type support for `Payment` and `Clawback` transactions
### Fixed
* `TransactionStream` model supports APIv2

View File

@@ -7,20 +7,32 @@ import type {
TransactionV1Stream,
TxResponse,
} from '..'
import type { Amount, APIVersion, DEFAULT_API_VERSION } from '../models/common'
import type {
Amount,
IssuedCurrency,
APIVersion,
DEFAULT_API_VERSION,
MPTAmount,
} from '../models/common'
import type {
AccountTxTransaction,
RequestResponseMap,
} from '../models/methods'
import { AccountTxVersionResponseMap } from '../models/methods/accountTx'
import { BaseRequest, BaseResponse } from '../models/methods/baseMethod'
import { PaymentFlags, Transaction } from '../models/transactions'
import { PaymentFlags, Transaction, isMPTAmount } from '../models/transactions'
import type { TransactionMetadata } from '../models/transactions/metadata'
import { isFlagEnabled } from '../models/utils'
const WARN_PARTIAL_PAYMENT_CODE = 2001
function amountsEqual(amt1: Amount, amt2: Amount): boolean {
/* eslint-disable complexity -- check different token types */
/* eslint-disable @typescript-eslint/consistent-type-assertions -- known currency type */
function amountsEqual(
amt1: Amount | MPTAmount,
amt2: Amount | MPTAmount,
): boolean {
// Compare XRP
if (typeof amt1 === 'string' && typeof amt2 === 'string') {
return amt1 === amt2
}
@@ -29,15 +41,32 @@ function amountsEqual(amt1: Amount, amt2: Amount): boolean {
return false
}
// Compare MPTs
if (isMPTAmount(amt1) && isMPTAmount(amt2)) {
const aValue = new BigNumber(amt1.value)
const bValue = new BigNumber(amt2.value)
return (
amt1.currency === amt2.currency &&
amt1.issuer === amt2.issuer &&
amt1.mpt_issuance_id === amt2.mpt_issuance_id && aValue.isEqualTo(bValue)
)
}
if (isMPTAmount(amt1) || isMPTAmount(amt2)) {
return false
}
// Compare issued currency (IOU)
const aValue = new BigNumber(amt1.value)
const bValue = new BigNumber(amt2.value)
return (
(amt1 as IssuedCurrency).currency === (amt2 as IssuedCurrency).currency &&
(amt1 as IssuedCurrency).issuer === (amt2 as IssuedCurrency).issuer &&
aValue.isEqualTo(bValue)
)
}
/* eslint-enable complexity */
/* eslint-enable @typescript-eslint/consistent-type-assertions */
function isPartialPayment(
tx?: Transaction,

View File

@@ -20,6 +20,11 @@ export interface IssuedCurrencyAmount extends IssuedCurrency {
value: string
}
export interface MPTAmount {
mpt_issuance_id: string
value: string
}
export type Amount = IssuedCurrencyAmount | string
export interface Balance {

View File

@@ -51,6 +51,8 @@ type LedgerEntryFilter =
| 'escrow'
| 'fee'
| 'hashes'
| 'mpt_issuance'
| 'mptoken'
| 'nft_offer'
| 'nft_page'
| 'offer'

View File

@@ -0,0 +1,11 @@
import { MPTAmount } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface MPToken extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'MPToken'
MPTokenIssuanceID: string
MPTAmount?: MPTAmount
Flags: number
OwnerNode?: string
}

View File

@@ -0,0 +1,13 @@
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface MPTokenIssuance extends BaseLedgerEntry, HasPreviousTxnID {
LedgerEntryType: 'MPTokenIssuance'
Flags: number
Issuer: string
AssetScale?: number
MaximumAmount?: string
OutstandingAmount: string
TransferFee?: number
MPTokenMetadata?: string
OwnerNode?: string
}

View File

@@ -18,6 +18,8 @@ import FeeSettings, {
import { Ledger, LedgerV1 } from './Ledger'
import { LedgerEntry, LedgerEntryFilter } from './LedgerEntry'
import LedgerHashes from './LedgerHashes'
import { MPToken } from './MPToken'
import { MPTokenIssuance } from './MPTokenIssuance'
import NegativeUNL, { NEGATIVE_UNL_ID } from './NegativeUNL'
import { NFTokenOffer } from './NFTokenOffer'
import { NFToken, NFTokenPage } from './NFTokenPage'
@@ -55,6 +57,8 @@ export {
Majority,
NEGATIVE_UNL_ID,
NegativeUNL,
MPTokenIssuance,
MPToken,
NFTokenOffer,
NFTokenPage,
NFToken,

View File

@@ -21,6 +21,22 @@ import { BaseRequest, BaseResponse, LookupByLedgerRequest } from './baseMethod'
*/
export interface LedgerEntryRequest extends BaseRequest, LookupByLedgerRequest {
command: 'ledger_entry'
/**
* Retrieve a MPTokenIssuance object from the ledger.
*/
mpt_issuance?: string
/**
* Retrieve a MPToken object from the ledger.
*/
mptoken?:
| {
mpt_issuance_id: string
account: string
}
| string
/**
* Retrieve an Automated Market Maker (AMM) object from the ledger.
* This is similar to amm_info method, but the ledger_entry version returns only the ledger entry as stored.

View File

@@ -0,0 +1,67 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
Account,
validateOptionalField,
isAccount,
GlobalFlags,
} from './common'
/**
* Transaction Flags for an MPTokenAuthorize Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenAuthorizeFlags {
/**
* If set and transaction is submitted by a holder, it indicates that the holder no
* longer wants to hold the MPToken, which will be deleted as a result. If the the holder's
* MPToken has non-zero balance while trying to set this flag, the transaction will fail. On
* the other hand, if set and transaction is submitted by an issuer, it would mean that the
* issuer wants to unauthorize the holder (only applicable for allow-listing),
* which would unset the lsfMPTAuthorized flag on the MPToken.
*/
tfMPTUnauthorize = 0x00000001,
}
/**
* Map of flags to boolean values representing {@link MPTokenAuthorize} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenAuthorizeFlagsInterface extends GlobalFlags {
tfMPTUnauthorize?: boolean
}
/**
* The MPTokenAuthorize transaction is used to globally lock/unlock a MPTokenIssuance,
* or lock/unlock an individual's MPToken.
*/
export interface MPTokenAuthorize extends BaseTransaction {
TransactionType: 'MPTokenAuthorize'
/**
* Identifies the MPTokenIssuance
*/
MPTokenIssuanceID: string
/**
* An optional XRPL Address of an individual token holder balance to lock/unlock.
* If omitted, this transaction will apply to all any accounts holding MPTs.
*/
Holder?: Account
Flags?: number | MPTokenAuthorizeFlagsInterface
}
/**
* Verify the form and type of an MPTokenAuthorize at runtime.
*
* @param tx - An MPTokenAuthorize Transaction.
* @throws When the MPTokenAuthorize is Malformed.
*/
export function validateMPTokenAuthorize(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
validateOptionalField(tx, 'Holder', isAccount)
}

View File

@@ -0,0 +1,161 @@
import { ValidationError } from '../../errors'
import { isHex, INTEGER_SANITY_CHECK } from '../utils'
import {
BaseTransaction,
GlobalFlags,
validateBaseTransaction,
validateOptionalField,
isString,
} from './common'
import type { TransactionMetadataBase } from './metadata'
// 2^63 - 1
const MAX_AMT = '9223372036854775807'
/**
* Transaction Flags for an MPTokenIssuanceCreate Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenIssuanceCreateFlags {
/**
* If set, indicates that the MPT can be locked both individually and globally.
* If not set, the MPT cannot be locked in any way.
*/
tfMPTCanLock = 0x00000002,
/**
* If set, indicates that individual holders must be authorized.
* This enables issuers to limit who can hold their assets.
*/
tfMPTRequireAuth = 0x00000004,
/**
* If set, indicates that individual holders can place their balances into an escrow.
*/
tfMPTCanEscrow = 0x00000008,
/**
* If set, indicates that individual holders can trade their balances
* using the XRP Ledger DEX or AMM.
*/
tfMPTCanTrade = 0x00000010,
/**
* If set, indicates that tokens may be transferred to other accounts
* that are not the issuer.
*/
tfMPTCanTransfer = 0x00000020,
/**
* If set, indicates that the issuer may use the Clawback transaction
* to clawback value from individual holders.
*/
tfMPTCanClawback = 0x00000040,
}
/**
* Map of flags to boolean values representing {@link MPTokenIssuanceCreate} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenIssuanceCreateFlagsInterface extends GlobalFlags {
tfMPTCanLock?: boolean
tfMPTRequireAuth?: boolean
tfMPTCanEscrow?: boolean
tfMPTCanTrade?: boolean
tfMPTCanTransfer?: boolean
tfMPTCanClawback?: boolean
}
/**
* The MPTokenIssuanceCreate transaction creates a MPTokenIssuance object
* and adds it to the relevant directory node of the creator account.
* This transaction is the only opportunity an issuer has to specify any token fields
* that are defined as immutable (e.g., MPT Flags). If the transaction is successful,
* the newly created token will be owned by the account (the creator account) which
* executed the transaction.
*/
export interface MPTokenIssuanceCreate extends BaseTransaction {
TransactionType: 'MPTokenIssuanceCreate'
/**
* An asset scale is the difference, in orders of magnitude, between a standard unit and
* a corresponding fractional unit. More formally, the asset scale is a non-negative integer
* (0, 1, 2, …) such that one standard unit equals 10^(-scale) of a corresponding
* fractional unit. If the fractional unit equals the standard unit, then the asset scale is 0.
* Note that this value is optional, and will default to 0 if not supplied.
*/
AssetScale?: number
/**
* Specifies the maximum asset amount of this token that should ever be issued.
* It is a non-negative integer string that can store a range of up to 63 bits. If not set, the max
* amount will default to the largest unsigned 63-bit integer (0x7FFFFFFFFFFFFFFF or 9223372036854775807)
*
* Example:
* ```
* MaximumAmount: '9223372036854775807'
* ```
*/
MaximumAmount?: string
/**
* Specifies the fee to charged by the issuer for secondary sales of the Token,
* if such sales are allowed. Valid values for this field are between 0 and 50,000 inclusive,
* allowing transfer rates of between 0.000% and 50.000% in increments of 0.001.
* The field must NOT be present if the `tfMPTCanTransfer` flag is not set.
*/
TransferFee?: number
/**
* Arbitrary metadata about this issuance, in hex format.
*/
MPTokenMetadata?: string | null
Flags?: number | MPTokenIssuanceCreateFlagsInterface
}
export interface MPTokenIssuanceCreateMetadata extends TransactionMetadataBase {
mpt_issuance_id?: string
}
/**
* Verify the form and type of an MPTokenIssuanceCreate at runtime.
*
* @param tx - An MPTokenIssuanceCreate Transaction.
* @throws When the MPTokenIssuanceCreate is Malformed.
*/
export function validateMPTokenIssuanceCreate(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'MaximumAmount', isString)
validateOptionalField(tx, 'MPTokenMetadata', isString)
if (typeof tx.MPTokenMetadata === 'string' && tx.MPTokenMetadata === '') {
throw new ValidationError(
'MPTokenIssuanceCreate: MPTokenMetadata must not be empty string',
)
}
if (typeof tx.MPTokenMetadata === 'string' && !isHex(tx.MPTokenMetadata)) {
throw new ValidationError(
'MPTokenIssuanceCreate: MPTokenMetadata must be in hex format',
)
}
if (typeof tx.MaximumAmount === 'string') {
if (!INTEGER_SANITY_CHECK.exec(tx.MaximumAmount)) {
throw new ValidationError('MPTokenIssuanceCreate: Invalid MaximumAmount')
} else if (
BigInt(tx.MaximumAmount) > BigInt(MAX_AMT) ||
BigInt(tx.MaximumAmount) < BigInt(`0`)
) {
throw new ValidationError(
'MPTokenIssuanceCreate: MaximumAmount out of range',
)
}
}
const MAX_TRANSFER_FEE = 50000
if (typeof tx.TransferFee === 'number') {
if (tx.TransferFee < 0 || tx.TransferFee > MAX_TRANSFER_FEE) {
throw new ValidationError(
'MPTokenIssuanceCreate: TransferFee out of range',
)
}
}
}

View File

@@ -0,0 +1,34 @@
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
} from './common'
/**
* The MPTokenIssuanceDestroy transaction is used to remove an MPTokenIssuance object
* from the directory node in which it is being held, effectively removing the token
* from the ledger. If this operation succeeds, the corresponding
* MPTokenIssuance is removed and the owners reserve requirement is reduced by one.
* This operation must fail if there are any holders who have non-zero balances.
*/
export interface MPTokenIssuanceDestroy extends BaseTransaction {
TransactionType: 'MPTokenIssuanceDestroy'
/**
* Identifies the MPTokenIssuance object to be removed by the transaction.
*/
MPTokenIssuanceID: string
}
/**
* Verify the form and type of an MPTokenIssuanceDestroy at runtime.
*
* @param tx - An MPTokenIssuanceDestroy Transaction.
* @throws When the MPTokenIssuanceDestroy is Malformed.
*/
export function validateMPTokenIssuanceDestroy(
tx: Record<string, unknown>,
): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
}

View File

@@ -0,0 +1,82 @@
import { ValidationError } from '../../errors'
import { isFlagEnabled } from '../utils'
import {
BaseTransaction,
isString,
validateBaseTransaction,
validateRequiredField,
Account,
validateOptionalField,
isAccount,
GlobalFlags,
} from './common'
/**
* Transaction Flags for an MPTokenIssuanceSet Transaction.
*
* @category Transaction Flags
*/
export enum MPTokenIssuanceSetFlags {
/**
* If set, indicates that issuer locks the MPT
*/
tfMPTLock = 0x00000001,
/**
* If set, indicates that issuer unlocks the MPT
*/
tfMPTUnlock = 0x00000002,
}
/**
* Map of flags to boolean values representing {@link MPTokenIssuanceSet} transaction
* flags.
*
* @category Transaction Flags
*/
export interface MPTokenIssuanceSetFlagsInterface extends GlobalFlags {
tfMPTLock?: boolean
tfMPTUnlock?: boolean
}
/**
* The MPTokenIssuanceSet transaction is used to globally lock/unlock a MPTokenIssuance,
* or lock/unlock an individual's MPToken.
*/
export interface MPTokenIssuanceSet extends BaseTransaction {
TransactionType: 'MPTokenIssuanceSet'
/**
* Identifies the MPTokenIssuance
*/
MPTokenIssuanceID: string
/**
* An optional XRPL Address of an individual token holder balance to lock/unlock.
* If omitted, this transaction will apply to all any accounts holding MPTs.
*/
Holder?: Account
Flags?: number | MPTokenIssuanceSetFlagsInterface
}
/**
* Verify the form and type of an MPTokenIssuanceSet at runtime.
*
* @param tx - An MPTokenIssuanceSet Transaction.
* @throws When the MPTokenIssuanceSet is Malformed.
*/
export function validateMPTokenIssuanceSet(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateRequiredField(tx, 'MPTokenIssuanceID', isString)
validateOptionalField(tx, 'Holder', isAccount)
if (typeof tx.Flags === 'number') {
const flags = tx.Flags
if (
isFlagEnabled(flags, MPTokenIssuanceSetFlags.tfMPTLock) &&
isFlagEnabled(flags, MPTokenIssuanceSetFlags.tfMPTUnlock)
) {
throw new ValidationError('MPTokenIssuanceSet: flag conflict')
}
} else {
throw new Error('tx.Flags is not a number')
}
}

View File

@@ -1,10 +1,13 @@
import { ValidationError } from '../../errors'
import { IssuedCurrencyAmount } from '../common'
import { IssuedCurrencyAmount, MPTAmount } from '../common'
import {
BaseTransaction,
validateBaseTransaction,
isIssuedCurrency,
isMPTAmount,
isAccount,
validateOptionalField,
} from './common'
/**
@@ -15,15 +18,20 @@ export interface Clawback extends BaseTransaction {
TransactionType: 'Clawback'
/**
* Indicates the AccountID that submitted this transaction. The account MUST
* be the issuer of the currency.
* be the issuer of the currency or MPT.
*/
Account: string
/**
* The amount of currency to deliver, and it must be non-XRP. The nested field
* names MUST be lower-case. The `issuer` field MUST be the holder's address,
* The amount of currency or MPT to clawback, and it must be non-XRP. The nested field
* names MUST be lower-case. If the amount is IOU, the `issuer` field MUST be the holder's address,
* whom to be clawed back.
*/
Amount: IssuedCurrencyAmount
Amount: IssuedCurrencyAmount | MPTAmount
/**
* Indicates the AccountID that the issuer wants to clawback. This field is only valid for clawing back
* MPTs.
*/
Holder?: string
}
/**
@@ -34,16 +42,29 @@ export interface Clawback extends BaseTransaction {
*/
export function validateClawback(tx: Record<string, unknown>): void {
validateBaseTransaction(tx)
validateOptionalField(tx, 'Holder', isAccount)
if (tx.Amount == null) {
throw new ValidationError('Clawback: missing field Amount')
}
if (!isIssuedCurrency(tx.Amount)) {
if (!isIssuedCurrency(tx.Amount) && !isMPTAmount(tx.Amount)) {
throw new ValidationError('Clawback: invalid Amount')
}
if (isIssuedCurrency(tx.Amount) && tx.Account === tx.Amount.issuer) {
throw new ValidationError('Clawback: invalid holder Account')
}
if (isMPTAmount(tx.Amount) && tx.Account === tx.Holder) {
throw new ValidationError('Clawback: invalid holder Account')
}
if (isIssuedCurrency(tx.Amount) && tx.Holder) {
throw new ValidationError('Clawback: cannot have Holder for currency')
}
if (isMPTAmount(tx.Amount) && !tx.Holder) {
throw new ValidationError('Clawback: missing Holder')
}
}

View File

@@ -9,6 +9,7 @@ import {
Memo,
Signer,
XChainBridge,
MPTAmount,
} from '../common'
import { onlyHasFields } from '../utils'
@@ -59,6 +60,7 @@ const XRP_CURRENCY_SIZE = 1
const ISSUE_SIZE = 2
const ISSUED_CURRENCY_SIZE = 3
const XCHAIN_BRIDGE_SIZE = 4
const MPTOKEN_SIZE = 2
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
@@ -119,6 +121,21 @@ export function isIssuedCurrency(
)
}
/**
* Verify the form and type of an MPT at runtime.
*
* @param input - The input to check the form and type of.
* @returns Whether the MPTAmount is properly formed.
*/
export function isMPTAmount(input: unknown): input is MPTAmount {
return (
isRecord(input) &&
Object.keys(input).length === MPTOKEN_SIZE &&
typeof input.value === 'string' &&
typeof input.mpt_issuance_id === 'string'
)
}
/**
* Must be a valid account address
*/
@@ -144,7 +161,11 @@ export function isAccount(account: unknown): account is Account {
* @returns Whether the Amount is properly formed.
*/
export function isAmount(amount: unknown): amount is Amount {
return typeof amount === 'string' || isIssuedCurrency(amount)
return (
typeof amount === 'string' ||
isIssuedCurrency(amount) ||
isMPTAmount(amount)
)
}
/**

View File

@@ -1,4 +1,4 @@
export { BaseTransaction } from './common'
export { BaseTransaction, isMPTAmount } from './common'
export {
validate,
PseudoTransaction,
@@ -39,6 +39,22 @@ export { EscrowCancel } from './escrowCancel'
export { EscrowCreate } from './escrowCreate'
export { EscrowFinish } from './escrowFinish'
export { EnableAmendment, EnableAmendmentFlags } from './enableAmendment'
export {
MPTokenAuthorize,
MPTokenAuthorizeFlags,
MPTokenAuthorizeFlagsInterface,
} from './MPTokenAuthorize'
export {
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlags,
MPTokenIssuanceCreateFlagsInterface,
} from './MPTokenIssuanceCreate'
export { MPTokenIssuanceDestroy } from './MPTokenIssuanceDestroy'
export {
MPTokenIssuanceSet,
MPTokenIssuanceSetFlags,
MPTokenIssuanceSetFlagsInterface,
} from './MPTokenIssuanceSet'
export { NFTokenAcceptOffer } from './NFTokenAcceptOffer'
export { NFTokenBurn } from './NFTokenBurn'
export { NFTokenCancelOffer } from './NFTokenCancelOffer'

View File

@@ -1,6 +1,10 @@
import { Amount } from '../common'
import { Amount, MPTAmount } from '../common'
import { BaseTransaction } from './common'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceCreateMetadata,
} from './MPTokenIssuanceCreate'
import {
NFTokenAcceptOffer,
NFTokenAcceptOfferMetadata,
@@ -79,9 +83,9 @@ export function isDeletedNode(node: Node): node is DeletedNode {
export interface TransactionMetadataBase {
AffectedNodes: Node[]
DeliveredAmount?: Amount
DeliveredAmount?: Amount | MPTAmount
// "unavailable" possible for transactions before 2014-01-20
delivered_amount?: Amount | 'unavailable'
delivered_amount?: Amount | MPTAmount | 'unavailable'
TransactionIndex: number
TransactionResult: string
}
@@ -97,4 +101,6 @@ export type TransactionMetadata<T extends BaseTransaction = Transaction> =
? NFTokenAcceptOfferMetadata
: T extends NFTokenCancelOffer
? NFTokenCancelOfferMetadata
: T extends MPTokenIssuanceCreate
? MPTokenIssuanceCreateMetadata
: TransactionMetadataBase

View File

@@ -1,5 +1,5 @@
import { ValidationError } from '../../errors'
import { Amount, Path } from '../common'
import { Amount, Path, MPTAmount } from '../common'
import { isFlagEnabled } from '../utils'
import {
@@ -116,7 +116,7 @@ export interface Payment extends BaseTransaction {
* names MUST be lower-case. If the tfPartialPayment flag is set, deliver up
* to this amount instead.
*/
Amount: Amount
Amount: Amount | MPTAmount
/** The unique address of the account receiving the payment. */
Destination: Account
/**
@@ -142,19 +142,19 @@ export interface Payment extends BaseTransaction {
* cross-currency/cross-issue payments. Must be omitted for XRP-to-XRP
* Payments.
*/
SendMax?: Amount
SendMax?: Amount | MPTAmount
/**
* Minimum amount of destination currency this transaction should deliver.
* Only valid if this is a partial payment. For non-XRP amounts, the nested
* field names are lower-case.
*/
DeliverMin?: Amount
DeliverMin?: Amount | MPTAmount
Flags?: number | PaymentFlagsInterface
}
export interface PaymentMetadata extends TransactionMetadataBase {
DeliveredAmount?: Amount
delivered_amount?: Amount | 'unavailable'
DeliveredAmount?: Amount | MPTAmount
delivered_amount?: Amount | MPTAmount | 'unavailable'
}
/**

View File

@@ -27,6 +27,19 @@ import { EscrowCancel, validateEscrowCancel } from './escrowCancel'
import { EscrowCreate, validateEscrowCreate } from './escrowCreate'
import { EscrowFinish, validateEscrowFinish } from './escrowFinish'
import { TransactionMetadata } from './metadata'
import { MPTokenAuthorize, validateMPTokenAuthorize } from './MPTokenAuthorize'
import {
MPTokenIssuanceCreate,
validateMPTokenIssuanceCreate,
} from './MPTokenIssuanceCreate'
import {
MPTokenIssuanceDestroy,
validateMPTokenIssuanceDestroy,
} from './MPTokenIssuanceDestroy'
import {
MPTokenIssuanceSet,
validateMPTokenIssuanceSet,
} from './MPTokenIssuanceSet'
import {
NFTokenAcceptOffer,
validateNFTokenAcceptOffer,
@@ -115,6 +128,10 @@ export type SubmittableTransaction =
| EscrowCancel
| EscrowCreate
| EscrowFinish
| MPTokenAuthorize
| MPTokenIssuanceCreate
| MPTokenIssuanceDestroy
| MPTokenIssuanceSet
| NFTokenAcceptOffer
| NFTokenBurn
| NFTokenCancelOffer
@@ -306,6 +323,22 @@ export function validate(transaction: Record<string, unknown>): void {
validateEscrowFinish(tx)
break
case 'MPTokenAuthorize':
validateMPTokenAuthorize(tx)
break
case 'MPTokenIssuanceCreate':
validateMPTokenIssuanceCreate(tx)
break
case 'MPTokenIssuanceDestroy':
validateMPTokenIssuanceDestroy(tx)
break
case 'MPTokenIssuanceSet':
validateMPTokenIssuanceSet(tx)
break
case 'NFTokenAcceptOffer':
validateNFTokenAcceptOffer(tx)
break

View File

@@ -9,6 +9,9 @@ import { AccountSetTfFlags } from '../transactions/accountSet'
import { AMMDepositFlags } from '../transactions/AMMDeposit'
import { AMMWithdrawFlags } from '../transactions/AMMWithdraw'
import { GlobalFlags } from '../transactions/common'
import { MPTokenAuthorizeFlags } from '../transactions/MPTokenAuthorize'
import { MPTokenIssuanceCreateFlags } from '../transactions/MPTokenIssuanceCreate'
import { MPTokenIssuanceSetFlags } from '../transactions/MPTokenIssuanceSet'
import { NFTokenCreateOfferFlags } from '../transactions/NFTokenCreateOffer'
import { NFTokenMintFlags } from '../transactions/NFTokenMint'
import { OfferCreateFlags } from '../transactions/offerCreate'
@@ -48,6 +51,9 @@ const txToFlag = {
AccountSet: AccountSetTfFlags,
AMMDeposit: AMMDepositFlags,
AMMWithdraw: AMMWithdrawFlags,
MPTokenAuthorize: MPTokenAuthorizeFlags,
MPTokenIssuanceCreate: MPTokenIssuanceCreateFlags,
MPTokenIssuanceSet: MPTokenIssuanceSetFlags,
NFTokenCreateOffer: NFTokenCreateOfferFlags,
NFTokenMint: NFTokenMintFlags,
OfferCreate: OfferCreateFlags,

View File

@@ -1,4 +1,5 @@
const HEX_REGEX = /^[0-9A-Fa-f]+$/u
export const INTEGER_SANITY_CHECK = /^[0-9]+$/u
/**
* Verify that all fields of an object are in fields.

View File

@@ -6,6 +6,11 @@ import {
TrustSet,
Payment,
Clawback,
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlags,
MPTokenAuthorize,
TransactionMetadata,
LedgerEntryResponse,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
@@ -112,4 +117,92 @@ describe('Clawback', function () {
},
TIMEOUT,
)
it(
'MPToken',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTCanClawback,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
const holderAuthTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, holderAuthTx, wallet2)
const paymentTx: Payment = {
TransactionType: 'Payment',
Account: testContext.wallet.classicAddress,
Amount: { mpt_issuance_id: mptID!, value: '9223372036854775807' },
Destination: wallet2.classicAddress,
}
await testTransaction(testContext.client, paymentTx, testContext.wallet)
let ledgerEntryResponse: LedgerEntryResponse =
await testContext.client.request({
command: 'ledger_entry',
mptoken: {
mpt_issuance_id: mptID!,
account: wallet2.classicAddress,
},
})
assert.equal(
// @ts-expect-error: Known issue with unknown object type
ledgerEntryResponse.result.node.MPTAmount,
'9223372036854775807',
)
// actual test - clawback
const clawTx: Clawback = {
TransactionType: 'Clawback',
Account: testContext.wallet.classicAddress,
Amount: {
mpt_issuance_id: mptID!,
value: '500',
},
Holder: wallet2.classicAddress,
}
await testTransaction(testContext.client, clawTx, testContext.wallet)
ledgerEntryResponse = await testContext.client.request({
command: 'ledger_entry',
mptoken: {
mpt_issuance_id: mptID!,
account: wallet2.classicAddress,
},
})
assert.equal(
// @ts-expect-error: Known issue with unknown object type
ledgerEntryResponse.result.node.MPTAmount,
'9223372036854775307',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,119 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenAuthorize,
MPTokenIssuanceCreateFlags,
MPTokenAuthorizeFlags,
TransactionMetadata,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction, generateFundedWallet } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('MPTokenAuthorize', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTRequireAuth,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
let authTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'mptoken',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Holder owns 1 MPToken on the ledger',
)
authTx = {
TransactionType: 'MPTokenAuthorize',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
Holder: wallet2.classicAddress,
}
await testTransaction(testContext.client, authTx, testContext.wallet)
authTx = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
0,
'Holder owns nothing on the ledger',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,54 @@
import { assert } from 'chai'
import { MPTokenIssuanceCreate } 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('MPTokenIssuanceCreate', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const tx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
// 0x7fffffffffffffff
MaximumAmount: '9223372036854775807',
AssetScale: 2,
}
await testTransaction(testContext.client, tx, testContext.wallet)
const accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
assert.equal(
// @ts-expect-error: Known issue with unknown object type
accountObjectsResponse.result.account_objects[0].MaximumAmount,
`9223372036854775807`,
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceDestroy,
TransactionMetadata,
} 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('MPTokenIssuanceDestroy', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const destroyTx: MPTokenIssuanceDestroy = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, destroyTx, testContext.wallet)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
0,
'Should be zero issuance on the ledger',
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,78 @@
import { assert } from 'chai'
import {
MPTokenIssuanceCreate,
MPTokenIssuanceSet,
MPTokenIssuanceCreateFlags,
MPTokenIssuanceSetFlags,
TransactionMetadata,
} 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('MPTokenIssuanceDestroy', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'base',
async () => {
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
const accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const setTx: MPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: testContext.wallet.classicAddress,
MPTokenIssuanceID: mptID!,
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
}
await testTransaction(testContext.client, setTx, testContext.wallet)
},
TIMEOUT,
)
})

View File

@@ -1,6 +1,12 @@
import { assert } from 'chai'
import { Payment, Wallet } from '../../../src'
import {
Payment,
Wallet,
MPTokenIssuanceCreate,
MPTokenAuthorize,
TransactionMetadata,
} from '../../../src'
import serverUrl from '../serverUrl'
import {
setupClient,
@@ -102,4 +108,89 @@ describe('Payment', function () {
},
TIMEOUT,
)
it(
'Validate MPT Payment ',
async () => {
const wallet2 = await generateFundedWallet(testContext.client)
const createTx: MPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: testContext.wallet.classicAddress,
}
const mptCreateRes = await testTransaction(
testContext.client,
createTx,
testContext.wallet,
)
const txHash = mptCreateRes.result.tx_json.hash
const txResponse = await testContext.client.request({
command: 'tx',
transaction: txHash,
})
const meta = txResponse.result
.meta as TransactionMetadata<MPTokenIssuanceCreate>
const mptID = meta.mpt_issuance_id
let accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Should be exactly one issuance on the ledger',
)
const authTx: MPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: wallet2.classicAddress,
MPTokenIssuanceID: mptID!,
}
await testTransaction(testContext.client, authTx, wallet2)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: wallet2.classicAddress,
type: 'mptoken',
})
assert.lengthOf(
accountObjectsResponse.result.account_objects,
1,
'Holder owns 1 MPToken on the ledger',
)
const payTx: Payment = {
TransactionType: 'Payment',
Account: testContext.wallet.classicAddress,
Destination: wallet2.classicAddress,
Amount: {
mpt_issuance_id: mptID!,
value: '100',
},
}
await testTransaction(testContext.client, payTx, testContext.wallet)
accountObjectsResponse = await testContext.client.request({
command: 'account_objects',
account: testContext.wallet.classicAddress,
type: 'mpt_issuance',
})
assert.equal(
// @ts-expect-error -- Object type not known
accountObjectsResponse.result.account_objects[0].OutstandingAmount,
`100`,
)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,63 @@
import { assert } from 'chai'
import { validate, ValidationError, MPTokenAuthorizeFlags } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenAuthorize Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenAuthorize', function () {
it(`verifies valid MPTokenAuthorize`, function () {
let validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
validMPTokenAuthorize = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
Flags: MPTokenAuthorizeFlags.tfMPTUnauthorize,
} as any
assert.doesNotThrow(() => validate(validMPTokenAuthorize))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenAuthorize',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenAuthorize: missing field MPTokenIssuanceID',
)
})
})

View File

@@ -0,0 +1,124 @@
import { assert } from 'chai'
import {
convertStringToHex,
validate,
ValidationError,
MPTokenIssuanceCreateFlags,
} from '../../src'
/**
* MPTokenIssuanceCreate Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceCreate', function () {
it(`verifies valid MPTokenIssuanceCreate`, function () {
const validMPTokenIssuanceCreate = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
// 0x7fffffffffffffff
MaximumAmount: '9223372036854775807',
AssetScale: 2,
TransferFee: 1,
Flags: 2,
MPTokenMetadata: convertStringToHex('http://xrpl.org'),
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceCreate))
})
it(`throws w/ MPTokenMetadata being an empty string`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
MPTokenMetadata: '',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MPTokenMetadata must not be empty string',
)
})
it(`throws w/ MPTokenMetadata not in hex format`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Flags: MPTokenIssuanceCreateFlags.tfMPTCanLock,
MPTokenMetadata: 'http://xrpl.org',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MPTokenMetadata must be in hex format',
)
})
it(`throws w/ Invalid MaximumAmount`, function () {
let invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '9223372036854775808',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: MaximumAmount out of range',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '-1',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: Invalid MaximumAmount',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MaximumAmount: '0x12',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: Invalid MaximumAmount',
)
})
it(`throws w/ Invalid TransferFee`, function () {
let invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: -1,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee out of range',
)
invalid = {
TransactionType: 'MPTokenIssuanceCreate',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
TransferFee: 50001,
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceCreate: TransferFee out of range',
)
})
})

View File

@@ -0,0 +1,35 @@
import { assert } from 'chai'
import { validate, ValidationError } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenIssuanceDestroy Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceDestroy', function () {
it(`verifies valid MPTokenIssuanceDestroy`, function () {
const validMPTokenIssuanceDestroy = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceDestroy))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceDestroy',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceDestroy: missing field MPTokenIssuanceID',
)
})
})

View File

@@ -0,0 +1,73 @@
import { assert } from 'chai'
import { validate, ValidationError, MPTokenIssuanceSetFlags } from '../../src'
const TOKEN_ID = '000004C463C52827307480341125DA0577DEFC38405B0E3E'
/**
* MPTokenIssuanceSet Transaction Verification Testing.
*
* Providing runtime verification testing for each specific transaction type.
*/
describe('MPTokenIssuanceSet', function () {
it(`verifies valid MPTokenIssuanceSet`, function () {
let validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
Flags: MPTokenIssuanceSetFlags.tfMPTLock,
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
// It's fine to not specify any flag, it means only tx fee is deducted
validMPTokenIssuanceSet = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG',
} as any
assert.doesNotThrow(() => validate(validMPTokenIssuanceSet))
})
it(`throws w/ missing MPTokenIssuanceID`, function () {
const invalid = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceSet: missing field MPTokenIssuanceID',
)
})
it(`throws w/ conflicting flags`, function () {
/* eslint-disable no-bitwise -- Bitwise operation needed for flag combination */
const invalid = {
TransactionType: 'MPTokenIssuanceSet',
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
MPTokenIssuanceID: TOKEN_ID,
Flags:
MPTokenIssuanceSetFlags.tfMPTLock | MPTokenIssuanceSetFlags.tfMPTUnlock,
} as any
/* eslint-enable no-bitwise -- Re-enable bitwise rule */
assert.throws(
() => validate(invalid),
ValidationError,
'MPTokenIssuanceSet: flag conflict',
)
})
})

View File

@@ -78,4 +78,72 @@ describe('Clawback', function () {
'Clawback: invalid holder Account',
)
})
it(`verifies valid MPT Clawback`, function () {
const validClawback = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.doesNotThrow(() => validate(validClawback))
})
it(`throws w/ invalid Holder Account`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: invalid holder Account',
)
})
it(`throws w/ invalid Holder`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: missing Holder',
)
})
it(`throws w/ invalid currency Holder`, function () {
const invalidAccount = {
TransactionType: 'Clawback',
Amount: {
currency: 'DSH',
issuer: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
value: '43.11584856965009',
},
Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm',
Holder: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.throws(
() => validate(invalidAccount),
ValidationError,
'Clawback: cannot have Holder for currency',
)
})
})

View File

@@ -258,4 +258,18 @@ describe('Payment', function () {
'PaymentTransaction: tfPartialPayment flag required with DeliverMin',
)
})
it(`verifies valid MPT PaymentTransaction`, function () {
const mptPaymentTransaction = {
TransactionType: 'Payment',
Account: 'rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo',
Amount: {
mpt_issuance_id: '000004C463C52827307480341125DA0577DEFC38405B0E3E',
value: '10',
},
Destination: 'rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy',
} as any
assert.doesNotThrow(() => validatePayment(mptPaymentTransaction))
assert.doesNotThrow(() => validate(mptPaymentTransaction))
})
})