diff --git a/packages/xrpl/src/utils/hashes/index.ts b/packages/xrpl/src/utils/hashes/index.ts index 9d32f6bfa1..5011c3debf 100644 --- a/packages/xrpl/src/utils/hashes/index.ts +++ b/packages/xrpl/src/utils/hashes/index.ts @@ -41,6 +41,57 @@ function currencyToHex(currency: string): string { return bytesToHex(Uint8Array.from(bytes)) } +/** + * Convert currency code to hex for Issue serialization. + * XRP uses 160-bit zero currency code, others use standard encoding. + * + * @param code - The currency code to convert. + * @returns The hex representation for use in Issue serialization. + */ +function toIssueCurrencyHex(code: string): string { + const upper = code.toUpperCase() + // Native XRP uses 160-bit zero currency code + if (upper === 'XRP') { + return '00'.repeat(20) + } + // If already a 160-bit hex code, normalize case + if (/^[0-9a-f]{40}$/iu.test(code)) { + return code.toUpperCase() + } + // Otherwise treat as 3-letter ISO-like code + return currencyToHex(upper) +} + +/** + * Convert a number, bigint, or string to a 64-bit hex string. + * + * @param id - The ID to convert (number, bigint, or string). + * @returns 64-bit hex string representation. + * @throws Error if the ID is out of range or invalid format. + */ +function toU64Hex(id: bigint | number | string): string { + let bi: bigint + if (typeof id === 'bigint') { + bi = id + } else if (typeof id === 'number') { + if (!Number.isSafeInteger(id) || id < 0) { + throw new Error( + 'claimID must be a non-negative safe integer, bigint, or decimal string', + ) + } + bi = BigInt(id) + } else { + const str = id.trim() + bi = + str.startsWith('0x') || str.startsWith('0X') ? BigInt(str) : BigInt(str) + } + const maxUint64 = BigInt('0xffffffffffffffff') + if (bi < BigInt(0) || bi > maxUint64) { + throw new Error('claimID out of range for uint64') + } + return bi.toString(16).toUpperCase().padStart(16, '0') +} + /** * Hash the given binary transaction data with the single-signing prefix. * @@ -185,4 +236,353 @@ export function hashPaymentChannel( ) } +/** + * Compute the hash of a Ticket. + * + * @param address - Account that created the Ticket. + * @param ticketSequence - The Ticket Sequence number. + * @returns Hash of the Ticket. + * @category Utilities + */ +export function hashTicket(address: string, ticketSequence: number): string { + return sha512Half( + ledgerSpaceHex('ticket') + + addressToHex(address) + + ticketSequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0'), + ) +} + +/** + * Compute the hash of a Check. + * + * @param address - Account that created the Check. + * @param sequence - Sequence number of the CheckCreate transaction. + * @returns Hash of the Check. + * @category Utilities + */ +export function hashCheck(address: string, sequence: number): string { + return sha512Half( + ledgerSpaceHex('check') + + addressToHex(address) + + sequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0'), + ) +} + +/** + * Compute the hash of a DepositPreauth entry. + * + * @param address - Account that granted the authorization. + * @param authorizedAddress - Account that was authorized. + * @returns Hash of the DepositPreauth entry. + * @category Utilities + */ +export function hashDepositPreauth( + address: string, + authorizedAddress: string, +): string { + return sha512Half( + ledgerSpaceHex('depositPreauth') + + addressToHex(address) + + addressToHex(authorizedAddress), + ) +} + +/** + * Compute the hash of an NFTokenPage. + * + * @param address - Account that owns the NFTokenPage. + * @param nfTokenIDLow96 - The low 96 bits of a representative NFTokenID. + * @returns Hash of the NFTokenPage. + * @throws Error if nfTokenIDLow96 is not a 24-character hex string. + * @category Utilities + */ +export function hashNFTokenPage( + address: string, + nfTokenIDLow96: string, +): string { + // Normalize and validate a 24-char hex (96 bits) + const normalized = nfTokenIDLow96.replace(/^0x/iu, '').toUpperCase() + if (!/^[0-9A-F]{24}$/u.test(normalized)) { + throw new Error('nfTokenIDLow96 must be a 24-character hex string') + } + return sha512Half( + ledgerSpaceHex('nfTokenPage') + addressToHex(address) + normalized, + ) +} + +/** + * Compute the hash of an NFTokenOffer. + * + * @param address - Account that created the NFTokenOffer. + * @param sequence - Sequence number of the NFTokenCreateOffer transaction. + * @returns Hash of the NFTokenOffer. + * @category Utilities + */ +export function hashNFTokenOffer(address: string, sequence: number): string { + return sha512Half( + ledgerSpaceHex('nfTokenOffer') + + addressToHex(address) + + sequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0'), + ) +} + +/** + * Compute the hash of an AMM root. + * + * @param currency1 - First currency in the AMM pool. + * @param currency2 - Second currency in the AMM pool. + * @param issuer1 - Issuer of the first currency (omit for XRP). + * @param issuer2 - Issuer of the second currency (omit for XRP). + * @returns Hash of the AMM root. + * @category Utilities + */ +export function hashAMMRoot( + currency1: string, + currency2: string, + issuer1?: string, + issuer2?: string, +): string { + const cur1Hex = toIssueCurrencyHex(currency1) + const cur2Hex = toIssueCurrencyHex(currency2) + const iss1Hex = issuer1 ? addressToHex(issuer1) : '00'.repeat(20) + const iss2Hex = issuer2 ? addressToHex(issuer2) : '00'.repeat(20) + + // Ensure deterministic ordering + const asset1 = cur1Hex + iss1Hex + const asset2 = cur2Hex + iss2Hex + const swap = new BigNumber(asset1, HEX).isGreaterThan( + new BigNumber(asset2, HEX), + ) + + const lowAsset = swap ? asset2 : asset1 + const highAsset = swap ? asset1 : asset2 + + return sha512Half(ledgerSpaceHex('ammRoot') + lowAsset + highAsset) +} + +/** + * Compute the hash of an Oracle entry. + * + * @param address - Account that created the Oracle. + * @param oracleID - The unique identifier for this Oracle. + * @returns Hash of the Oracle. + * @throws Error if oracleID is not a 64-character uppercase hex string. + * @category Utilities + */ +export function hashOracle(address: string, oracleID: string): string { + const normalized = oracleID.replace(/^0x/iu, '').toUpperCase() + // eslint-disable-next-line require-unicode-regexp -- Targeting older ES version + if (!/^[0-9A-F]{64}$/.test(normalized)) { + throw new Error( + 'oracleID must be a 64-character uppercase hexadecimal string', + ) + } + return sha512Half( + ledgerSpaceHex('oracle') + addressToHex(address) + normalized, + ) +} + +/** + * Compute the hash of a Hook entry. + * + * @param address - Account that installed the Hook. + * @param hookHash - Hash of the Hook code. + * @returns Hash of the Hook entry. + * @throws Error if hookHash is not a 64-character hex string. + * @category Utilities + */ +export function hashHook(address: string, hookHash: string): string { + const normalized = hookHash.replace(/^0x/iu, '').toUpperCase() + // eslint-disable-next-line require-unicode-regexp -- Targeting older ES version + if (!/^[0-9A-F]{64}$/.test(normalized)) { + throw new Error('hookHash must be a 64-character hexadecimal string') + } + return sha512Half(ledgerSpaceHex('hook') + addressToHex(address) + normalized) +} + +/** + * Compute the hash of a Hook State entry. + * + * @param address - Account that owns the Hook State. + * @param hookHash - Hash of the Hook code. + * @param hookStateKey - Key for the Hook State entry. + * @returns Hash of the Hook State entry. + * @throws Error if hookHash or hookStateKey are not 64-character hex strings. + * @category Utilities + */ +export function hashHookState( + address: string, + hookHash: string, + hookStateKey: string, +): string { + const hookHashNorm = hookHash.replace(/^0x/iu, '').toUpperCase() + const stateKeyNorm = hookStateKey.replace(/^0x/iu, '').toUpperCase() + // eslint-disable-next-line require-unicode-regexp -- Targeting older ES version + if (!/^[0-9A-F]{64}$/.test(hookHashNorm)) { + throw new Error('hookHash must be a 64-character hexadecimal string') + } + // eslint-disable-next-line require-unicode-regexp -- Targeting older ES version + if (!/^[0-9A-F]{64}$/.test(stateKeyNorm)) { + throw new Error('hookStateKey must be a 64-character hexadecimal string') + } + return sha512Half( + ledgerSpaceHex('hookState') + + addressToHex(address) + + hookHashNorm + + stateKeyNorm, + ) +} + +/** + * Compute the hash of a Hook Definition entry. + * + * @param hookHash - Hash of the Hook code. + * @returns Hash of the Hook Definition entry. + * @throws Error if hookHash is not a 64-character hex string. + * @category Utilities + */ +export function hashHookDefinition(hookHash: string): string { + // Validate that hookHash is a 64-character hex string + if (!/^[0-9A-F]{64}$/iu.test(hookHash)) { + throw new Error('hookHash must be a 64-character hexadecimal string') + } + return sha512Half(ledgerSpaceHex('hookDefinition') + hookHash) +} + +/** + * Compute the hash of a DID entry. + * + * @param address - Account that owns the DID. + * @returns Hash of the DID entry. + * @category Utilities + */ +export function hashDID(address: string): string { + return sha512Half(ledgerSpaceHex('did') + addressToHex(address)) +} + +/** + * Compute the hash of a Bridge entry. + * + * @param door - The door account of the bridge. + * @param otherChainSource - The source account on the other chain. + * @param issuingChainDoor - The door account on the issuing chain. + * @param issuingChainIssue - The issue specification on the issuing chain. + * @param lockingChainDoor - The door account on the locking chain. + * @param lockingChainIssue - The issue specification on the locking chain. + * @returns Hash of the Bridge entry. + * @category Utilities + */ +export function hashBridge( + door: string, + otherChainSource: string, + issuingChainDoor: string, + issuingChainIssue: string, + lockingChainDoor: string, + lockingChainIssue: string, +): string { + return sha512Half( + ledgerSpaceHex('bridge') + + addressToHex(door) + + addressToHex(otherChainSource) + + addressToHex(issuingChainDoor) + + currencyToHex(issuingChainIssue) + + addressToHex(lockingChainDoor) + + currencyToHex(lockingChainIssue), + ) +} + +/** + * Compute the hash of an XChain Owned Claim ID entry. + * + * @param address - Account that owns the claim ID. + * @param bridgeAccount - The bridge account. + * @param claimID - The claim ID. + * @returns Hash of the XChain Owned Claim ID entry. + * @category Utilities + */ +export function hashXChainOwnedClaimID( + address: string, + bridgeAccount: string, + claimID: bigint | number | string, +): string { + return sha512Half( + ledgerSpaceHex('xchainOwnedClaimID') + + addressToHex(address) + + addressToHex(bridgeAccount) + + toU64Hex(claimID), + ) +} + +/** + * Compute the hash of an XChain Owned Create Account Claim ID entry. + * + * @param address - Account that owns the create account claim ID. + * @param bridgeAccount - The bridge account. + * @param claimID - The claim ID. + * @returns Hash of the XChain Owned Create Account Claim ID entry. + * @category Utilities + */ +export function hashXChainOwnedCreateAccountClaimID( + address: string, + bridgeAccount: string, + claimID: bigint | number | string, +): string { + return sha512Half( + ledgerSpaceHex('xchainOwnedCreateAccountClaimID') + + addressToHex(address) + + addressToHex(bridgeAccount) + + toU64Hex(claimID), + ) +} + +/** + * Compute the hash of an MPToken entry. + * + * @param address - Account that holds the MPToken. + * @param mpTokenIssuanceID - The MPToken issuance ID. + * @returns Hash of the MPToken entry. + * @throws Error if mpTokenIssuanceID is not a 64-character hex string. + * @category Utilities + */ +export function hashMPToken( + address: string, + mpTokenIssuanceID: string, +): string { + // Validate that mpTokenIssuanceID is a 64-character hex string + if (!/^[0-9A-F]{64}$/iu.test(mpTokenIssuanceID)) { + throw new Error( + 'mpTokenIssuanceID must be a 64-character hexadecimal string', + ) + } + return sha512Half( + ledgerSpaceHex('mpToken') + addressToHex(address) + mpTokenIssuanceID, + ) +} + +/** + * Compute the hash of an MPToken Issuance entry. + * + * @param sequence - Sequence number of the transaction that created this issuance. + * @param address - Account that created the MPToken issuance. + * @returns Hash of the MPToken Issuance entry. + * @category Utilities + */ +export function hashMPTokenIssuance(sequence: number, address: string): string { + return sha512Half( + ledgerSpaceHex('mpTokenIssuance') + + sequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0') + + addressToHex(address), + ) +} + +/** + * Compute the hash of a NegativeUNL entry. + * + * @returns Hash of the NegativeUNL entry. + * @category Utilities + */ +export function hashNegativeUNL(): string { + return sha512Half(ledgerSpaceHex('negativeUNL')) +} + export { hashLedgerHeader, hashSignedTx, hashLedger, hashStateTree, hashTxTree } diff --git a/packages/xrpl/src/utils/hashes/ledgerSpaces.ts b/packages/xrpl/src/utils/hashes/ledgerSpaces.ts index e2af0c6aae..b5eac9ce14 100644 --- a/packages/xrpl/src/utils/hashes/ledgerSpaces.ts +++ b/packages/xrpl/src/utils/hashes/ledgerSpaces.ts @@ -29,6 +29,21 @@ const ledgerSpaces = { paychan: 'x', check: 'C', depositPreauth: 'p', + // Additional ledger entry types + negativeUNL: 'N', + nfTokenPage: 'P', + nfTokenOffer: 't', + ammRoot: 'A', + oracle: 'R', + hook: 'H', + hookState: 'h', + hookDefinition: 'D', + did: 'I', + bridge: 'X', + xchainOwnedClaimID: 'K', + xchainOwnedCreateAccountClaimID: 'k', + mpToken: 'M', + mpTokenIssuance: 'm', } export default ledgerSpaces diff --git a/packages/xrpl/test/utils/hashes.test.ts b/packages/xrpl/test/utils/hashes.test.ts index edc19575fa..4d6ee109ce 100644 --- a/packages/xrpl/test/utils/hashes.test.ts +++ b/packages/xrpl/test/utils/hashes.test.ts @@ -21,6 +21,23 @@ import { hashAccountRoot, hashOfferId, hashSignerListId, + hashTicket, + hashCheck, + hashDepositPreauth, + hashNFTokenPage, + hashNFTokenOffer, + hashAMMRoot, + hashOracle, + hashHook, + hashHookState, + hashHookDefinition, + hashDID, + hashBridge, + hashXChainOwnedClaimID, + hashXChainOwnedCreateAccountClaimID, + hashMPToken, + hashMPTokenIssuance, + hashNegativeUNL, } from '../../src/utils/hashes' import fixtures from '../fixtures/rippled' import { assertResultMatch } from '../testUtils' @@ -230,4 +247,270 @@ describe('Hashes', function () { '9EDF5DB29F536DD3919037F1E8A72B040D075571A10C9000294C57B5ECEEA791', ) }) + + describe('New Ledger Entry Hash Functions', function () { + /* eslint-disable require-unicode-regexp -- TypeScript target doesn't support u flag in tests */ + it('hashTicket', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const ticketSequence = 123 + const actualHash = hashTicket(account, ticketSequence) + + // Ticket hashes should be deterministic + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.isTrue( + /^[A-F0-9]+$/.test(actualHash), + 'Hash should be uppercase hex', + ) + + // Should be consistent + assert.equal(hashTicket(account, ticketSequence), actualHash) + }) + + it('hashCheck', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const sequence = 456 + const actualHash = hashCheck(account, sequence) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal(hashCheck(account, sequence), actualHash) + }) + + it('hashDepositPreauth', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const authorizedAddress = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY' + const actualHash = hashDepositPreauth(account, authorizedAddress) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal(hashDepositPreauth(account, authorizedAddress), actualHash) + + // Different order should give different hash + const reverseHash = hashDepositPreauth(authorizedAddress, account) + assert.notEqual(actualHash, reverseHash, 'Order should matter') + }) + + it('hashNFTokenPage', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const nfTokenIDLow96 = '0008138800000000C1ECF2F9' + const actualHash = hashNFTokenPage(account, nfTokenIDLow96) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashNFTokenPage(account, nfTokenIDLow96), actualHash) + }) + + it('hashNFTokenOffer', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const sequence = 789 + const actualHash = hashNFTokenOffer(account, sequence) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal(hashNFTokenOffer(account, sequence), actualHash) + }) + + it('hashAMMRoot - XRP/USD pair', function () { + const currency1 = 'XRP' + const currency2 = 'USD' + const issuer2 = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const actualHash = hashAMMRoot(currency1, currency2, undefined, issuer2) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent regardless of order + const reverseHash = hashAMMRoot(currency2, currency1, issuer2, undefined) + assert.equal( + actualHash, + reverseHash, + 'Order should not matter for AMM pairs', + ) + }) + + it('hashAMMRoot - USD/EUR pair', function () { + const currency1 = 'USD' + const currency2 = 'EUR' + const issuer1 = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const issuer2 = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY' + const actualHash = hashAMMRoot(currency1, currency2, issuer1, issuer2) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent regardless of order + const reverseHash = hashAMMRoot(currency2, currency1, issuer2, issuer1) + assert.equal( + actualHash, + reverseHash, + 'Order should not matter for AMM pairs', + ) + }) + + it('hashOracle', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const oracleID = + '61E8E8ED53FA2CEBE192B23897071E9A75217BF5A410E9CB5B45AAB7AECA567A' + const actualHash = hashOracle(account, oracleID) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashOracle(account, oracleID), actualHash) + }) + + it('hashHook', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const hookHash = + 'CA4562711E4679FE9317DD767871E90A404C7A8B84FAFD35EC2CF0231F1F6DAF' + const actualHash = hashHook(account, hookHash) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashHook(account, hookHash), actualHash) + }) + + it('hashHookState', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const hookHash = + 'CA4562711E4679FE9317DD767871E90A404C7A8B84FAFD35EC2CF0231F1F6DAF' + const hookStateKey = + '458101D51051230B1D56E9ACAFAA34451BF65FA000F95DF6F0FF5B3A62D83FC2' + const actualHash = hashHookState(account, hookHash, hookStateKey) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashHookState(account, hookHash, hookStateKey), actualHash) + }) + + it('hashHookDefinition', function () { + const hookHash = + 'CA4562711E4679FE9317DD767871E90A404C7A8B84FAFD35EC2CF0231F1F6DAF' + const actualHash = hashHookDefinition(hookHash) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashHookDefinition(hookHash), actualHash) + }) + + it('hashDID', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const actualHash = hashDID(account) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal(hashDID(account), actualHash) + }) + + it('hashBridge', function () { + const door = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const otherChainSource = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY' + const issuingChainDoor = 'r32UufnaCGL82HubijgJGDmdE5hac7ZvLw' + const issuingChainIssue = 'USD' + const lockingChainDoor = 'rDx69ebzbowuqztksVDmZXjizTd12BVr4x' + const lockingChainIssue = 'EUR' + + const actualHash = hashBridge( + door, + otherChainSource, + issuingChainDoor, + issuingChainIssue, + lockingChainDoor, + lockingChainIssue, + ) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal( + hashBridge( + door, + otherChainSource, + issuingChainDoor, + issuingChainIssue, + lockingChainDoor, + lockingChainIssue, + ), + actualHash, + ) + }) + + it('hashXChainOwnedClaimID', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const bridgeAccount = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY' + const claimID = 12345 + const actualHash = hashXChainOwnedClaimID(account, bridgeAccount, claimID) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal( + hashXChainOwnedClaimID(account, bridgeAccount, claimID), + actualHash, + ) + }) + + it('hashXChainOwnedCreateAccountClaimID', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const bridgeAccount = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY' + const claimID = 67890 + const actualHash = hashXChainOwnedCreateAccountClaimID( + account, + bridgeAccount, + claimID, + ) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal( + hashXChainOwnedCreateAccountClaimID(account, bridgeAccount, claimID), + actualHash, + ) + }) + + it('hashMPToken', function () { + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const mpTokenIssuanceID = + '9EDF5DB29F536DD3919037F1E8A72B040D075571A10C9000294C57B5ECEEA791' + const actualHash = hashMPToken(account, mpTokenIssuanceID) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + // Should be consistent + assert.equal(hashMPToken(account, mpTokenIssuanceID), actualHash) + }) + + it('hashMPTokenIssuance', function () { + const sequence = 98765 + const account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' + const actualHash = hashMPTokenIssuance(sequence, account) + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent + assert.equal(hashMPTokenIssuance(sequence, account), actualHash) + }) + + it('hashNegativeUNL', function () { + const actualHash = hashNegativeUNL() + + assert.equal(actualHash.length, 64, 'Hash should be 64 characters') + assert.match(actualHash, /^[A-F0-9]+$/, 'Hash should be uppercase hex') + + // Should be consistent (same hash every time since no parameters) + assert.equal(hashNegativeUNL(), actualHash) + }) + /* eslint-enable require-unicode-regexp */ + }) })