diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index d9a27f95..e84a4164 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -9,6 +9,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr * Infinite error/reconnect in browser if an exception was raised during the initial websocket connection event. * Errors during reliable submission with no error message now properly show the preliminary result instead of a type error * Fixed serialize/deserialize verification bug in `Wallet.sign()` when signing a non-XRP Payment with an amount that contains trailing insignificant zeros +* Allow lowercase hex values for `NFTokenMint.URI` ## 2.2.3 (2022-05-04) ### Fixed diff --git a/packages/xrpl/src/Wallet/index.ts b/packages/xrpl/src/Wallet/index.ts index 189aeaad..bf394e4f 100644 --- a/packages/xrpl/src/Wallet/index.ts +++ b/packages/xrpl/src/Wallet/index.ts @@ -26,6 +26,7 @@ import { import ECDSA from '../ECDSA' import { ValidationError } from '../errors' import { Transaction } from '../models/transactions' +import { isHex } from '../models/utils' import { ensureClassicAddress } from '../sugar/utils' import { hashSignedTx } from '../utils/hashes/hashLedger' @@ -392,7 +393,7 @@ class Wallet { * @throws A ValidationError if the transaction does not have a TxnSignature/Signers property, or if * the serialized Transaction desn't match the original transaction. */ - // eslint-disable-next-line class-methods-use-this -- Helper for organization purposes + // eslint-disable-next-line class-methods-use-this, max-lines-per-function -- Helper for organization purposes private checkTxSerialization(serialized: string, tx: Transaction): void { // Decode the serialized transaction: const decoded = decode(serialized) @@ -440,7 +441,15 @@ class Wallet { return memo }) - if (!_.isEqual(decoded, tx)) { + + if (txCopy.TransactionType === 'NFTokenMint' && txCopy.URI) { + if (!isHex(txCopy.URI)) { + throw new ValidationError('URI must be a hex value') + } + txCopy.URI = txCopy.URI.toUpperCase() + } + + if (!_.isEqual(decoded, txCopy)) { const data = { decoded, tx, diff --git a/packages/xrpl/src/models/transactions/NFTokenMint.ts b/packages/xrpl/src/models/transactions/NFTokenMint.ts index f7d1d6c2..c6c688cb 100644 --- a/packages/xrpl/src/models/transactions/NFTokenMint.ts +++ b/packages/xrpl/src/models/transactions/NFTokenMint.ts @@ -1,4 +1,5 @@ import { ValidationError } from '../../errors' +import { isHex } from '../utils' import { BaseTransaction, GlobalFlags, validateBaseTransaction } from './common' @@ -105,6 +106,10 @@ export function validateNFTokenMint(tx: Record): void { ) } + if (typeof tx.URI === 'string' && !isHex(tx.URI)) { + throw new ValidationError('NFTokenMint: URI must be in hex format') + } + if (tx.NFTokenTaxon == null) { throw new ValidationError('NFTokenMint: missing field NFTokenTaxon') } diff --git a/packages/xrpl/src/models/utils/index.ts b/packages/xrpl/src/models/utils/index.ts index 81b6a637..cb797197 100644 --- a/packages/xrpl/src/models/utils/index.ts +++ b/packages/xrpl/src/models/utils/index.ts @@ -1,3 +1,5 @@ +const HEX_REGEX = /^[0-9A-Fa-f]+$/u + /** * Verify that all fields of an object are in fields. * @@ -23,3 +25,13 @@ export function isFlagEnabled(Flags: number, checkFlag: number): boolean { // eslint-disable-next-line no-bitwise -- flags needs bitwise return (checkFlag & Flags) === checkFlag } + +/** + * Check if string is in hex format. + * + * @param str - The string to check if it's in hex format. + * @returns True if string is in hex format + */ +export function isHex(str: string): boolean { + return HEX_REGEX.test(str) +} diff --git a/packages/xrpl/test/models/NFTokenMint.ts b/packages/xrpl/test/models/NFTokenMint.ts index 9142bb16..80f395f8 100644 --- a/packages/xrpl/test/models/NFTokenMint.ts +++ b/packages/xrpl/test/models/NFTokenMint.ts @@ -66,4 +66,24 @@ describe('NFTokenMint', function () { 'NFTokenMint: Issuer must not be equal to Account', ) }) + + it(`throws w/ URI not in hex format`, function () { + const invalid = { + TransactionType: 'NFTokenMint', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + Fee: '5000000', + Sequence: 2470665, + Flags: NFTokenMintFlags.tfTransferable, + NFTokenTaxon: 0, + Issuer: 'r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ', + TransferFee: 1, + URI: 'http://xrpl.org', + } as any + + assert.throws( + () => validate(invalid), + ValidationError, + 'NFTokenMint: URI must be in hex format', + ) + }) }) diff --git a/packages/xrpl/test/wallet/index.ts b/packages/xrpl/test/wallet/index.ts index 4767e0e4..0b0ed31d 100644 --- a/packages/xrpl/test/wallet/index.ts +++ b/packages/xrpl/test/wallet/index.ts @@ -1,6 +1,6 @@ import { assert } from 'chai' import { decode } from 'ripple-binary-codec/dist' -import { Transaction } from 'xrpl-local' +import { NFTokenMint, Transaction } from 'xrpl-local' import ECDSA from 'xrpl-local/ECDSA' import Wallet from 'xrpl-local/Wallet' @@ -639,6 +639,60 @@ describe('Wallet', function () { assert.deepEqual(result, expectedResult) }) + + it('sign allows lowercase hex value for NFTokenMint.URI', async function () { + const tx: NFTokenMint = { + TransactionType: 'NFTokenMint', + Account: wallet.address, + TransferFee: 314, + NFTokenTaxon: 0, + Flags: 8, + Fee: '10', + URI: '697066733a2f2f62616679626569676479727a74357366703775646d37687537367568377932366e6634646675796c71616266336f636c67747179353566627a6469', + Memos: [ + { + Memo: { + MemoType: + '687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963', + MemoData: '72656e74', + }, + }, + ], + } + const result = wallet.sign(tx) + const expectedResult = { + tx_blob: + '12001914013A2200000008202A0000000068400000000000000A732102A8A44DB3D4C73EEEE11DFE54D2029103B776AA8A8D293A91D645977C9DF5F5447446304402203795B6E9D6D0086FB26E2C6B7A8C02D50B8560D45C9D5C80DF271D3349515E5302203B0898A7D8C06243D7C2116D2011ACB68DF3123BB7336D6C27269FD388C12CC07542697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A64698114B3263BD0A9BF9DFDBBBBD07F536355FF477BF0E9F9EA7C1F687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E657269637D0472656E74E1F1', + hash: '2F359B3CFD1CE6D7BFB672F8ADCE98FE964B1FD04CFC337177FB3D8FBE889788', + } + + assert.deepEqual(result, expectedResult) + }) + + it('sign throws when NFTokenMint.URI is not a hex value', async function () { + const tx: NFTokenMint = { + TransactionType: 'NFTokenMint', + Account: wallet.address, + TransferFee: 314, + NFTokenTaxon: 0, + Flags: 8, + Fee: '10', + URI: 'ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf4dfuylqabf3oclgtqy55fbzdi', + Memos: [ + { + Memo: { + MemoType: + '687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963', + MemoData: '72656e74', + }, + }, + ], + } + + assert.throws(() => { + wallet.sign(tx) + }, /URI must be a hex value/u) + }) }) describe('verifyTransaction', function () {