mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-20 12:15:51 +00:00
Lint utils directory (#1563)
* Lint the utils directory * Modify computeLedgerHash to take new Ledger format.
This commit is contained in:
committed by
Mayukha Vadari
parent
33f83947f1
commit
633032ddd8
16
package-lock.json
generated
16
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"@types/puppeteer": "5.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"@xrplf/eslint-config": "^1.2.2",
|
||||
"@xrplf/eslint-config": "^1.3",
|
||||
"@xrplf/prettier-config": "^1.2.0",
|
||||
"assert": "^2.0.0",
|
||||
"buffer": "^6.0.2",
|
||||
@@ -1315,9 +1315,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xrplf/eslint-config": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xrplf/eslint-config/-/eslint-config-1.2.2.tgz",
|
||||
"integrity": "sha512-mZ4m0Q/3eE5dKbC1z875yrrcq9/X6O1g8uxqJSYSoWPY1AlwvfqSLm6qoe4CJ5v8eaOGFNWQGWF6a4na8pYoDw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@xrplf/eslint-config/-/eslint-config-1.3.0.tgz",
|
||||
"integrity": "sha512-Zk3ZgzHxj8vozA87LF6Gv4B9v8KXH51Q4oW2sG4fCFHsqiSWVG+n4ZPr7DhTqPNhaxV21yL3OqsT7G6FgD1wXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"confusing-browser-globals": "^1.0.9",
|
||||
@@ -1340,7 +1340,7 @@
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.5",
|
||||
"prettier": "^2.0.5",
|
||||
"typescript": "^3.9.3"
|
||||
"typescript": ">=3.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xrplf/prettier-config": {
|
||||
@@ -9981,9 +9981,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@xrplf/eslint-config": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xrplf/eslint-config/-/eslint-config-1.2.2.tgz",
|
||||
"integrity": "sha512-mZ4m0Q/3eE5dKbC1z875yrrcq9/X6O1g8uxqJSYSoWPY1AlwvfqSLm6qoe4CJ5v8eaOGFNWQGWF6a4na8pYoDw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@xrplf/eslint-config/-/eslint-config-1.3.0.tgz",
|
||||
"integrity": "sha512-Zk3ZgzHxj8vozA87LF6Gv4B9v8KXH51Q4oW2sG4fCFHsqiSWVG+n4ZPr7DhTqPNhaxV21yL3OqsT7G6FgD1wXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"confusing-browser-globals": "^1.0.9",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@types/puppeteer": "5.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.0.0",
|
||||
"@xrplf/eslint-config": "^1.2.2",
|
||||
"@xrplf/eslint-config": "^1.3",
|
||||
"@xrplf/prettier-config": "^1.2.0",
|
||||
"assert": "^2.0.0",
|
||||
"buffer": "^6.0.2",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { classicAddressToXAddress } from 'ripple-address-codec'
|
||||
import { deriveKeypair, deriveAddress } from 'ripple-keypairs'
|
||||
|
||||
function deriveXAddress(options: {
|
||||
interface DeriveOptions {
|
||||
publicKey: string
|
||||
tag: number | false
|
||||
test: boolean
|
||||
}): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an X-Address from a public key and a destination tag.
|
||||
*
|
||||
* @param options - Public key and destination tag to encode as an X-Address.
|
||||
* @returns X-Address.
|
||||
*/
|
||||
function deriveXAddress(options: DeriveOptions): string {
|
||||
const classicAddress = deriveAddress(options.publicKey)
|
||||
return classicAddressToXAddress(classicAddress, options.tag, options.test)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { classicAddressToXAddress } from 'ripple-address-codec'
|
||||
import keypairs from 'ripple-keypairs'
|
||||
|
||||
import { errors } from '../common'
|
||||
import ECDSA from '../common/ecdsa'
|
||||
import { UnexpectedError } from '../common/errors'
|
||||
|
||||
export interface GeneratedAddress {
|
||||
xAddress: string
|
||||
classicAddress?: string
|
||||
address?: string // @deprecated Use `classicAddress` instead.
|
||||
secret: string
|
||||
}
|
||||
|
||||
@@ -27,7 +26,14 @@ export interface GenerateAddressOptions {
|
||||
includeClassicAddress?: boolean
|
||||
}
|
||||
|
||||
// TODO: move this function to be a static function of the Wallet class (Along with its helper data types)
|
||||
/**
|
||||
* TODO: Move this function to be a static function of the Wallet Class.
|
||||
* TODO: Doc this function.
|
||||
*
|
||||
* @param options - Options for generating X-Address.
|
||||
* @returns A generated address.
|
||||
* @throws When cannot generate an address.
|
||||
*/
|
||||
function generateXAddress(
|
||||
options: GenerateAddressOptions = {},
|
||||
): GeneratedAddress {
|
||||
@@ -44,7 +50,7 @@ function generateXAddress(
|
||||
const secret = keypairs.generateSeed(generateSeedOptions)
|
||||
const keypair = keypairs.deriveKeypair(secret)
|
||||
const classicAddress = keypairs.deriveAddress(keypair.publicKey)
|
||||
const returnValue: any = {
|
||||
const returnValue: GeneratedAddress = {
|
||||
xAddress: classicAddressToXAddress(
|
||||
classicAddress,
|
||||
false,
|
||||
@@ -54,11 +60,14 @@ function generateXAddress(
|
||||
}
|
||||
if (options.includeClassicAddress) {
|
||||
returnValue.classicAddress = classicAddress
|
||||
returnValue.address = classicAddress
|
||||
}
|
||||
return returnValue
|
||||
} catch (error) {
|
||||
throw new errors.UnexpectedError(error.message)
|
||||
if (error instanceof Error) {
|
||||
throw new UnexpectedError(error.message)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Compute the hash of an account, given the account's classic address (starting wi
|
||||
|
||||
Compute the hash of an account's SignerList.
|
||||
|
||||
### computeOrderID = (address: string, sequence: number): string
|
||||
### computeOfferID = (address: string, sequence: number): string
|
||||
|
||||
Compute the hash of an order, given the owner's classic address (starting with `r`) and the account sequence number of the `OfferCreate` order transaction.
|
||||
|
||||
|
||||
@@ -12,29 +12,29 @@
|
||||
*/
|
||||
|
||||
enum HashPrefix {
|
||||
// transaction plus signature to give transaction ID
|
||||
TRANSACTION_ID = 0x54584e00, // 'TXN'
|
||||
// transaction plus signature to give transaction ID 'TXN'
|
||||
TRANSACTION_ID = 0x54584e00,
|
||||
|
||||
// transaction plus metadata
|
||||
TRANSACTION_NODE = 0x534e4400, // 'TND'
|
||||
// transaction plus metadata 'TND'
|
||||
TRANSACTION_NODE = 0x534e4400,
|
||||
|
||||
// inner node in tree
|
||||
INNER_NODE = 0x4d494e00, // 'MIN'
|
||||
// inner node in tree 'MIN'
|
||||
INNER_NODE = 0x4d494e00,
|
||||
|
||||
// leaf node in tree
|
||||
LEAF_NODE = 0x4d4c4e00, // 'MLN'
|
||||
// leaf node in tree 'MLN'
|
||||
LEAF_NODE = 0x4d4c4e00,
|
||||
|
||||
// inner transaction to sign
|
||||
TRANSACTION_SIGN = 0x53545800, // 'STX'
|
||||
// inner transaction to sign 'STX'
|
||||
TRANSACTION_SIGN = 0x53545800,
|
||||
|
||||
// inner transaction to sign (TESTNET)
|
||||
TRANSACTION_SIGN_TESTNET = 0x73747800, // 'stx'
|
||||
// inner transaction to sign (TESTNET) 'stx'
|
||||
TRANSACTION_SIGN_TESTNET = 0x73747800,
|
||||
|
||||
// inner transaction to multisign
|
||||
TRANSACTION_MULTISIGN = 0x534d5400, // 'SMT'
|
||||
// inner transaction to multisign 'SMT'
|
||||
TRANSACTION_MULTISIGN = 0x534d5400,
|
||||
|
||||
// ledger
|
||||
LEDGER = 0x4c575200, // 'LWR'
|
||||
// ledger 'LWR'
|
||||
LEDGER = 0x4c575200,
|
||||
}
|
||||
|
||||
export default HashPrefix
|
||||
|
||||
@@ -1,104 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers -- this file mimics
|
||||
behavior in rippled. Magic numbers are used for lengths and conditions */
|
||||
/* eslint-disable no-bitwise -- this file mimics behavior in rippled. It uses
|
||||
bitwise operators for and-ing numbers with a mask and bit shifting. */
|
||||
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { decodeAccountID } from 'ripple-address-codec'
|
||||
import { decode, encode } from 'ripple-binary-codec'
|
||||
|
||||
import { ValidationError } from '../../common/errors'
|
||||
import { Transaction } from '../../models/transactions'
|
||||
|
||||
import HashPrefix from './hashPrefix'
|
||||
import computeLedgerHash, {
|
||||
computeLedgerHeaderHash,
|
||||
computeSignedTransactionHash,
|
||||
computeTransactionTreeHash,
|
||||
computeStateTreeHash,
|
||||
} from './ledgerHash'
|
||||
import ledgerSpaces from './ledgerSpaces'
|
||||
import sha512Half from './sha512Half'
|
||||
import { SHAMap, NodeType } from './shamap'
|
||||
|
||||
const BITS_IN_HEX = 16
|
||||
const HEX = 16
|
||||
const BYTE_LENGTH = 4
|
||||
|
||||
function padLeftZero(string: string, length: number): string {
|
||||
return Array(length - string.length + 1).join('0') + string
|
||||
}
|
||||
|
||||
function intToHex(integer: number, byteLength: number): string {
|
||||
return padLeftZero(Number(integer).toString(BITS_IN_HEX), byteLength * 2)
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: number[]): string {
|
||||
return Buffer.from(bytes).toString('hex')
|
||||
}
|
||||
|
||||
function bigintToHex(
|
||||
integerString: string | number | BigNumber,
|
||||
byteLength: number,
|
||||
): string {
|
||||
const hex = new BigNumber(integerString).toString(16)
|
||||
return padLeftZero(hex, byteLength * 2)
|
||||
}
|
||||
|
||||
function ledgerSpaceHex(name: string): string {
|
||||
return intToHex(ledgerSpaces[name].charCodeAt(0), 2)
|
||||
}
|
||||
|
||||
function addressToHex(address: string): string {
|
||||
return Buffer.from(decodeAccountID(address)).toString('hex')
|
||||
}
|
||||
|
||||
function currencyToHex(currency: string): string {
|
||||
if (currency.length === 3) {
|
||||
const bytes = new Array(20 + 1).join('0').split('').map(parseFloat)
|
||||
bytes[12] = currency.charCodeAt(0) & 0xff
|
||||
bytes[13] = currency.charCodeAt(1) & 0xff
|
||||
bytes[14] = currency.charCodeAt(2) & 0xff
|
||||
return bytesToHex(bytes)
|
||||
function ledgerSpaceHex(name: keyof typeof ledgerSpaces): string {
|
||||
return ledgerSpaces[name].charCodeAt(0).toString(HEX).padStart(4, '0')
|
||||
}
|
||||
|
||||
const MASK = 0xff
|
||||
function currencyToHex(currency: string): string {
|
||||
if (currency.length !== 3) {
|
||||
return currency
|
||||
}
|
||||
|
||||
function addLengthPrefix(hex: string): string {
|
||||
const length = hex.length / 2
|
||||
if (length <= 192) {
|
||||
return bytesToHex([length]) + hex
|
||||
}
|
||||
if (length <= 12480) {
|
||||
const x = length - 193
|
||||
// eslint-disable-next-line no-bitwise -- adding a prefix to hex requires bitwise operations
|
||||
return bytesToHex([193 + (x >>> 8), x & 0xff]) + hex
|
||||
}
|
||||
if (length <= 918744) {
|
||||
const x = length - 12481
|
||||
// eslint-disable-next-line no-bitwise -- adding a prefix to hex requires bitwise operations
|
||||
return bytesToHex([241 + (x >>> 16), (x >>> 8) & 0xff, x & 0xff]) + hex
|
||||
}
|
||||
throw new Error('Variable integer overflow.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the Transaction object as the ledger does. Throws if the transaction is unsigned.
|
||||
*
|
||||
* @param tx - A transaction to hash. Tx may be in binary blob form. Tx must be signed.
|
||||
* @returns A hash of tx.
|
||||
* @throws ValidationError if the Transaction is unsigned.
|
||||
*/
|
||||
|
||||
export function computeSignedTransactionHash(tx: Transaction | string): string {
|
||||
let txBlob
|
||||
let txObject
|
||||
if (typeof tx === 'string') {
|
||||
txBlob = tx
|
||||
txObject = decode(tx)
|
||||
} else {
|
||||
txBlob = encode(tx)
|
||||
txObject = tx
|
||||
}
|
||||
|
||||
if (
|
||||
txObject.TxnSignature === undefined &&
|
||||
(txObject.Signers === undefined ||
|
||||
txObject.Signers[0].Signer.TxnSignature === undefined)
|
||||
) {
|
||||
throw new ValidationError('The transaction must be signed to hash it.')
|
||||
}
|
||||
|
||||
const prefix = HashPrefix.TRANSACTION_ID.toString(16).toUpperCase()
|
||||
return sha512Half(prefix.concat(txBlob))
|
||||
const bytes = Array(20).fill(0)
|
||||
bytes[12] = currency.charCodeAt(0) & MASK
|
||||
bytes[13] = currency.charCodeAt(1) & MASK
|
||||
bytes[14] = currency.charCodeAt(2) & MASK
|
||||
return Buffer.from(bytes).toString('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,19 +49,19 @@ export function computeSignedTransactionHash(tx: Transaction | string): string {
|
||||
* @returns The hash to sign.
|
||||
*/
|
||||
export function computeBinaryTransactionSigningHash(txBlobHex: string): string {
|
||||
const prefix = HashPrefix.TRANSACTION_SIGN.toString(16).toUpperCase()
|
||||
const prefix = HashPrefix.TRANSACTION_SIGN.toString(HEX).toUpperCase()
|
||||
return sha512Half(prefix + txBlobHex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute Account Root Index.
|
||||
* Compute AccountRoot Ledger Object Index.
|
||||
*
|
||||
* All objects in a ledger's state tree have a unique index.
|
||||
* The Account Root index is derived by hashing the
|
||||
* All objects in a ledger's state tree have a unique Index.
|
||||
* The AccountRoot Ledger Object Index is derived by hashing the
|
||||
* address with a namespace identifier. This ensures every
|
||||
* index is unique.
|
||||
* Index is unique.
|
||||
*
|
||||
* See [Ledger Object IDs](https://xrpl.org/ledger-object-ids.html).
|
||||
* See [Ledger Object Indexes](https://xrpl.org/ledger-object-ids.html).
|
||||
*
|
||||
* @param address - The classic account address.
|
||||
* @returns The Ledger Object Index for the account.
|
||||
@@ -132,43 +71,56 @@ export function computeAccountRootIndex(address: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* [SignerList ID Format](https://xrpl.org/signerlist.html#signerlist-id-format).
|
||||
* [SignerList Index Format](https://xrpl.org/signerlist.html#signerlist-id-format).
|
||||
*
|
||||
* The index of a SignerList object is the SHA-512Half of the following values, concatenated in order:
|
||||
* The Index of a SignerList object is the SHA-512Half of the following values, concatenated in order:
|
||||
* * The RippleState space key (0x0053)
|
||||
* * The AccountID of the owner of the SignerList
|
||||
* * The SignerListID (currently always 0).
|
||||
*
|
||||
* This method computes a SignerList index.
|
||||
* This method computes a SignerList Ledger Object Index.
|
||||
*
|
||||
* @param address - The classic account address of the SignerList owner (starting with r).
|
||||
* @returns The ID of the account's SignerList object.
|
||||
* @returns The Index of the account's SignerList object.
|
||||
*/
|
||||
export function computeSignerListIndex(address: string): string {
|
||||
return sha512Half(
|
||||
`${ledgerSpaceHex('signerList') + addressToHex(address)}00000000`,
|
||||
) // uint32(0) signer list index
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Offer ID Format](https://xrpl.org/offer.html#offer-id-format).
|
||||
* [Offer Index Format](https://xrpl.org/offer.html#offer-id-format).
|
||||
*
|
||||
* The index of a Offer object is the SHA-512Half of the following values, concatenated in order:
|
||||
* The Index of a Offer object is the SHA-512Half of the following values, concatenated in order:
|
||||
* * The Offer space key (0x006F)
|
||||
* * The AccountID of the account placing the offer
|
||||
* * The Sequence number of the OfferCreate transaction that created the offer.
|
||||
*
|
||||
* This method computes an Offer Index (aka Order Index).
|
||||
* This method computes an Offer Index.
|
||||
*
|
||||
* @param address - The classic account address of the SignerList owner (starting with r).
|
||||
* @param sequence
|
||||
* @returns The index of the account's Offer object.
|
||||
* @param sequence - Sequence of the Offer.
|
||||
* @returns The Index of the account's Offer object.
|
||||
*/
|
||||
export function computeOfferIndex(address: string, sequence: number): string {
|
||||
const prefix = `00${intToHex(ledgerSpaces.offer.charCodeAt(0), 1)}`
|
||||
return sha512Half(prefix + addressToHex(address) + intToHex(sequence, 4))
|
||||
const hexPrefix = ledgerSpaces.offer
|
||||
.charCodeAt(0)
|
||||
.toString(HEX)
|
||||
.padStart(2, '0')
|
||||
const hexSequence = sequence.toString(HEX).padStart(8, '0')
|
||||
const prefix = `00${hexPrefix}`
|
||||
return sha512Half(prefix + addressToHex(address) + hexSequence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of a Trustline.
|
||||
*
|
||||
* @param address1 - One of the addresses in the Trustline.
|
||||
* @param address2 - The other address in the Trustline.
|
||||
* @param currency - Currency in the Trustline.
|
||||
* @returns The hash of the Trustline.
|
||||
*/
|
||||
export function computeTrustlineHash(
|
||||
address1: string,
|
||||
address2: string,
|
||||
@@ -189,56 +141,29 @@ export function computeTrustlineHash(
|
||||
)
|
||||
}
|
||||
|
||||
export function computeTransactionTreeHash(transactions: any[]): string {
|
||||
const shamap = new SHAMap()
|
||||
|
||||
transactions.forEach((txJSON) => {
|
||||
const txBlobHex = encode(txJSON)
|
||||
const metaHex = encode(txJSON.metaData)
|
||||
const txHash = computeSignedTransactionHash(txBlobHex)
|
||||
const data = addLengthPrefix(txBlobHex) + addLengthPrefix(metaHex)
|
||||
shamap.addItem(txHash, data, NodeType.TRANSACTION_METADATA)
|
||||
})
|
||||
|
||||
return shamap.hash
|
||||
}
|
||||
|
||||
export function computeStateTreeHash(entries: any[]): string {
|
||||
const shamap = new SHAMap()
|
||||
|
||||
entries.forEach((ledgerEntry) => {
|
||||
const data = encode(ledgerEntry)
|
||||
shamap.addItem(ledgerEntry.index, data, NodeType.ACCOUNT_STATE)
|
||||
})
|
||||
|
||||
return shamap.hash
|
||||
}
|
||||
|
||||
// see rippled Ledger::calculateLedgerHash()
|
||||
export function computeLedgerHash(ledgerHeader): string {
|
||||
const prefix = HashPrefix.LEDGER.toString(16).toUpperCase()
|
||||
return sha512Half(
|
||||
prefix +
|
||||
intToHex(ledgerHeader.ledger_index, 4) +
|
||||
bigintToHex(ledgerHeader.total_coins, 8) +
|
||||
ledgerHeader.parent_hash +
|
||||
ledgerHeader.transaction_hash +
|
||||
ledgerHeader.account_hash +
|
||||
intToHex(ledgerHeader.parent_close_time, 4) +
|
||||
intToHex(ledgerHeader.close_time, 4) +
|
||||
intToHex(ledgerHeader.close_time_resolution, 1) +
|
||||
intToHex(ledgerHeader.close_flags, 1),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the Hash of an Escrow LedgerEntry.
|
||||
*
|
||||
* @param address - Address of the Escrow.
|
||||
* @param sequence - OfferSequence of the Escrow.
|
||||
* @returns The hash of the Escrow LedgerEntry.
|
||||
*/
|
||||
export function computeEscrowHash(address: string, sequence: number): string {
|
||||
return sha512Half(
|
||||
ledgerSpaceHex('escrow') +
|
||||
addressToHex(address) +
|
||||
intToHex(sequence, BYTE_LENGTH),
|
||||
sequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0'),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of a Payment Channel.
|
||||
*
|
||||
* @param address - Account of the Payment Channel.
|
||||
* @param dstAddress - Destination Account of the Payment Channel.
|
||||
* @param sequence - Sequence number of the Transaction that created the Payment Channel.
|
||||
* @returns Hash of the Payment Channel.
|
||||
*/
|
||||
export function computePaymentChannelHash(
|
||||
address: string,
|
||||
dstAddress: string,
|
||||
@@ -248,6 +173,14 @@ export function computePaymentChannelHash(
|
||||
ledgerSpaceHex('paychan') +
|
||||
addressToHex(address) +
|
||||
addressToHex(dstAddress) +
|
||||
intToHex(sequence, BYTE_LENGTH),
|
||||
sequence.toString(HEX).padStart(BYTE_LENGTH * 2, '0'),
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
computeLedgerHeaderHash,
|
||||
computeSignedTransactionHash,
|
||||
computeLedgerHash,
|
||||
computeStateTreeHash,
|
||||
computeTransactionTreeHash,
|
||||
}
|
||||
|
||||
230
src/utils/hashes/ledgerHash.ts
Normal file
230
src/utils/hashes/ledgerHash.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers -- this file mimics
|
||||
behavior in rippled. Magic numbers are used for lengths and conditions */
|
||||
/* eslint-disable no-bitwise -- this file mimics behavior in rippled. It uses
|
||||
bitwise operators for and-ing numbers with a mask and bit shifting. */
|
||||
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { decode, encode } from 'ripple-binary-codec'
|
||||
|
||||
import { ValidationError } from '../../common/errors'
|
||||
import type { Ledger } from '../../models/ledger'
|
||||
import { LedgerEntry } from '../../models/ledger'
|
||||
import { Transaction } from '../../models/transactions'
|
||||
import Metadata from '../../models/transactions/metadata'
|
||||
|
||||
import HashPrefix from './hashPrefix'
|
||||
import sha512Half from './sha512Half'
|
||||
import SHAMap, { NodeType } from './shaMap'
|
||||
|
||||
const HEX = 16
|
||||
|
||||
interface ComputeLedgerHeaderHashOptions {
|
||||
computeTreeHashes?: boolean
|
||||
}
|
||||
|
||||
function intToHex(integer: number, byteLength: number): string {
|
||||
const foo = Number(integer)
|
||||
.toString(HEX)
|
||||
.padStart(byteLength * 2, '0')
|
||||
|
||||
return foo
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: number[]): string {
|
||||
return Buffer.from(bytes).toString('hex')
|
||||
}
|
||||
|
||||
function bigintToHex(
|
||||
integerString: string | number | BigNumber,
|
||||
byteLength: number,
|
||||
): string {
|
||||
const hex = new BigNumber(integerString).toString(HEX)
|
||||
return hex.padStart(byteLength * 2, '0')
|
||||
}
|
||||
|
||||
function addLengthPrefix(hex: string): string {
|
||||
const length = hex.length / 2
|
||||
if (length <= 192) {
|
||||
return bytesToHex([length]) + hex
|
||||
}
|
||||
if (length <= 12480) {
|
||||
const prefix = length - 193
|
||||
return bytesToHex([193 + (prefix >>> 8), prefix & 0xff]) + hex
|
||||
}
|
||||
if (length <= 918744) {
|
||||
const prefix = length - 12481
|
||||
return (
|
||||
bytesToHex([
|
||||
241 + (prefix >>> 16),
|
||||
(prefix >>> 8) & 0xff,
|
||||
prefix & 0xff,
|
||||
]) + hex
|
||||
)
|
||||
}
|
||||
throw new Error('Variable integer overflow.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the Transaction object as the ledger does. Throws if the transaction is unsigned.
|
||||
*
|
||||
* @param tx - A transaction to hash. Tx may be in binary blob form. Tx must be signed.
|
||||
* @returns A hash of tx.
|
||||
* @throws ValidationError if the Transaction is unsigned.
|
||||
*/
|
||||
export function computeSignedTransactionHash(tx: Transaction | string): string {
|
||||
let txBlob: string
|
||||
let txObject: Transaction
|
||||
if (typeof tx === 'string') {
|
||||
txBlob = tx
|
||||
// TODO: type ripple-binary-codec with Transaction
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required until updated in binary codec. */
|
||||
txObject = decode(tx) as unknown as Transaction
|
||||
} else {
|
||||
txBlob = encode(tx)
|
||||
txObject = tx
|
||||
}
|
||||
|
||||
if (txObject.TxnSignature === undefined && txObject.Signers === undefined) {
|
||||
throw new ValidationError('The transaction must be signed to hash it.')
|
||||
}
|
||||
|
||||
const prefix = HashPrefix.TRANSACTION_ID.toString(16).toUpperCase()
|
||||
return sha512Half(prefix.concat(txBlob))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of a ledger.
|
||||
*
|
||||
* @param ledgerHeader - Ledger to compute the hash of.
|
||||
* @returns The hash of the ledger.
|
||||
*/
|
||||
export function computeLedgerHeaderHash(ledgerHeader: Ledger): string {
|
||||
const prefix = HashPrefix.LEDGER.toString(HEX).toUpperCase()
|
||||
|
||||
const ledger =
|
||||
prefix +
|
||||
intToHex(Number(ledgerHeader.ledger_index), 4) +
|
||||
bigintToHex(ledgerHeader.total_coins, 8) +
|
||||
ledgerHeader.parent_hash +
|
||||
ledgerHeader.transaction_hash +
|
||||
ledgerHeader.account_hash +
|
||||
intToHex(ledgerHeader.parent_close_time, 4) +
|
||||
intToHex(ledgerHeader.close_time, 4) +
|
||||
intToHex(ledgerHeader.close_time_resolution, 1) +
|
||||
intToHex(ledgerHeader.close_flags, 1)
|
||||
|
||||
return sha512Half(ledger)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the root hash of the SHAMap containing all transactions.
|
||||
*
|
||||
* @param transactions - List of Transactions.
|
||||
* @returns The root hash of the SHAMap.
|
||||
*/
|
||||
export function computeTransactionTreeHash(
|
||||
transactions: Array<Transaction & { metaData?: Metadata }>,
|
||||
): string {
|
||||
const shamap = new SHAMap()
|
||||
for (const txJSON of transactions) {
|
||||
const txBlobHex = encode(txJSON)
|
||||
const metaHex = encode(txJSON.metaData ?? {})
|
||||
const txHash = computeSignedTransactionHash(txBlobHex)
|
||||
const data = addLengthPrefix(txBlobHex) + addLengthPrefix(metaHex)
|
||||
shamap.addItem(txHash, data, NodeType.TRANSACTION_METADATA)
|
||||
}
|
||||
|
||||
return shamap.hash
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the state hash of a list of LedgerEntries.
|
||||
*
|
||||
* @param entries - List of LedgerEntries.
|
||||
* @returns Hash of SHAMap that consists of all entries.
|
||||
*/
|
||||
export function computeStateTreeHash(entries: LedgerEntry[]): string {
|
||||
const shamap = new SHAMap()
|
||||
|
||||
entries.forEach((ledgerEntry) => {
|
||||
const data = encode(ledgerEntry)
|
||||
shamap.addItem(ledgerEntry.index, data, NodeType.ACCOUNT_STATE)
|
||||
})
|
||||
|
||||
return shamap.hash
|
||||
}
|
||||
|
||||
export function computeTransactionHash(
|
||||
ledger: Ledger,
|
||||
options: ComputeLedgerHeaderHashOptions,
|
||||
): string {
|
||||
const { transaction_hash } = ledger
|
||||
|
||||
if (!options.computeTreeHashes) {
|
||||
return transaction_hash
|
||||
}
|
||||
|
||||
if (ledger.transactions == null) {
|
||||
throw new ValidationError('transactions is missing from the ledger')
|
||||
}
|
||||
|
||||
const transactionHash = computeTransactionTreeHash(ledger.transactions)
|
||||
|
||||
if (transaction_hash !== transactionHash) {
|
||||
throw new ValidationError(
|
||||
'transactionHash in header' +
|
||||
' does not match computed hash of transactions',
|
||||
{
|
||||
transactionHashInHeader: transaction_hash,
|
||||
computedHashOfTransactions: transactionHash,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return transactionHash
|
||||
}
|
||||
|
||||
export function computeStateHash(
|
||||
ledger: Ledger,
|
||||
options: ComputeLedgerHeaderHashOptions,
|
||||
): string {
|
||||
const { account_hash } = ledger
|
||||
|
||||
if (!options.computeTreeHashes) {
|
||||
return account_hash
|
||||
}
|
||||
|
||||
if (ledger.accountState == null) {
|
||||
throw new ValidationError('accountState is missing from the ledger')
|
||||
}
|
||||
|
||||
const stateHash = computeStateTreeHash(ledger.accountState)
|
||||
|
||||
if (account_hash !== stateHash) {
|
||||
throw new ValidationError(
|
||||
'stateHash in header does not match computed hash of state',
|
||||
)
|
||||
}
|
||||
|
||||
return stateHash
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of a ledger.
|
||||
*
|
||||
* @param ledger - Ledger to compute the hash for.
|
||||
* @param options - Allow client to recompute Transaction and State Hashes.
|
||||
* @returns The has of ledger.
|
||||
*/
|
||||
function computeLedgerHash(
|
||||
ledger: Ledger,
|
||||
options: ComputeLedgerHeaderHashOptions = {},
|
||||
): string {
|
||||
const subhashes = {
|
||||
transaction_hash: computeTransactionHash(ledger, options),
|
||||
account_hash: computeStateHash(ledger, options),
|
||||
}
|
||||
return computeLedgerHeaderHash({ ...ledger, ...subhashes })
|
||||
}
|
||||
|
||||
export default computeLedgerHash
|
||||
@@ -8,14 +8,17 @@
|
||||
*
|
||||
* See [LedgerNameSpace enum](https://github.com/ripple/rippled/blob/master/src/ripple/protocol/LedgerFormats.h#L100).
|
||||
*/
|
||||
export default {
|
||||
const ledgerSpaces = {
|
||||
account: 'a',
|
||||
dirNode: 'd',
|
||||
generatorMap: 'g',
|
||||
rippleState: 'r',
|
||||
offer: 'o', // Entry for an offer.
|
||||
ownerDir: 'O', // Directory of things owned by an account.
|
||||
bookDir: 'B', // Directory of order books.
|
||||
// Entry for an offer.
|
||||
offer: 'o',
|
||||
// Directory of things owned by an account.
|
||||
ownerDir: 'O',
|
||||
// Directory of order books.
|
||||
bookDir: 'B',
|
||||
contract: 'c',
|
||||
skipList: 's',
|
||||
escrow: 'u',
|
||||
@@ -27,3 +30,5 @@ export default {
|
||||
check: 'C',
|
||||
depositPreauth: 'p',
|
||||
}
|
||||
|
||||
export default ledgerSpaces
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
const sha512Half = (hex: string): string => {
|
||||
const HASH_SIZE = 64
|
||||
|
||||
/**
|
||||
* Compute a sha512Half Hash of a hex string.
|
||||
*
|
||||
* @param hex - Hex string to hash.
|
||||
* @returns Hash of hex.
|
||||
*/
|
||||
function sha512Half(hex: string): string {
|
||||
return createHash('sha512')
|
||||
.update(Buffer.from(hex, 'hex'))
|
||||
.digest('hex')
|
||||
.toUpperCase()
|
||||
.slice(0, 64)
|
||||
.slice(0, HASH_SIZE)
|
||||
}
|
||||
|
||||
export default sha512Half
|
||||
|
||||
37
src/utils/hashes/shaMap/index.ts
Normal file
37
src/utils/hashes/shaMap/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import InnerNode from './innerNode'
|
||||
import Leaf from './leafNode'
|
||||
import { NodeType } from './node'
|
||||
|
||||
class SHAMap {
|
||||
public root: InnerNode
|
||||
|
||||
/**
|
||||
* SHAMap tree constructor.
|
||||
*/
|
||||
public constructor() {
|
||||
this.root = new InnerNode(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the SHAMap.
|
||||
*
|
||||
* @param tag - Index of the Node to add.
|
||||
* @param data - Data to insert into the tree.
|
||||
* @param type - Type of the node to add.
|
||||
*/
|
||||
public addItem(tag: string, data: string, type: NodeType): void {
|
||||
this.root.addItem(tag, new Leaf(tag, data, type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of the SHAMap.
|
||||
*
|
||||
* @returns The hash of the root of the SHAMap.
|
||||
*/
|
||||
public get hash(): string {
|
||||
return this.root.hash
|
||||
}
|
||||
}
|
||||
|
||||
export * from './node'
|
||||
export default SHAMap
|
||||
123
src/utils/hashes/shaMap/innerNode.ts
Normal file
123
src/utils/hashes/shaMap/innerNode.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import hashPrefix from '../hashPrefix'
|
||||
import sha512Half from '../sha512Half'
|
||||
|
||||
import Leaf from './leafNode'
|
||||
import { NodeType, Node } from './node'
|
||||
|
||||
const HEX_ZERO =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
const SLOT_MAX = 15
|
||||
const HEX = 16
|
||||
|
||||
/**
|
||||
* Class for SHAMap InnerNode.
|
||||
*/
|
||||
class InnerNode extends Node {
|
||||
public leaves: { [slot: number]: Node | undefined }
|
||||
public type: NodeType
|
||||
public depth: number
|
||||
public empty: boolean
|
||||
|
||||
/**
|
||||
* Define an Inner (non-leaf) node in a SHAMap tree.
|
||||
*
|
||||
* @param depth - I.e. How many parent inner nodes.
|
||||
*/
|
||||
public constructor(depth = 0) {
|
||||
super()
|
||||
this.leaves = {}
|
||||
this.type = NodeType.INNER
|
||||
this.depth = depth
|
||||
this.empty = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the InnerNode.
|
||||
*
|
||||
* @param tag - Equates to a ledger entry `index`.
|
||||
* @param node - Node to add.
|
||||
* @throws If there is a index collision.
|
||||
*/
|
||||
public addItem(tag: string, node: Node): void {
|
||||
const existingNode = this.getNode(parseInt(tag[this.depth], HEX))
|
||||
|
||||
if (existingNode === undefined) {
|
||||
this.setNode(parseInt(tag[this.depth], HEX), node)
|
||||
return
|
||||
}
|
||||
|
||||
// A node already exists in this slot
|
||||
if (existingNode instanceof InnerNode) {
|
||||
// There is an inner node, so we need to go deeper
|
||||
existingNode.addItem(tag, node)
|
||||
} else if (existingNode instanceof Leaf) {
|
||||
if (existingNode.tag === tag) {
|
||||
// Collision
|
||||
throw new Error(
|
||||
'Tried to add a node to a SHAMap that was already in there.',
|
||||
)
|
||||
} else {
|
||||
const newInnerNode = new InnerNode(this.depth + 1)
|
||||
|
||||
// Parent new and existing node
|
||||
newInnerNode.addItem(existingNode.tag, existingNode)
|
||||
newInnerNode.addItem(tag, node)
|
||||
|
||||
// And place the newly created inner node in the slot
|
||||
this.setNode(parseInt(tag[this.depth], HEX), newInnerNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the node that is currently in a given slot.
|
||||
*
|
||||
* @param slot - A number 0-15.
|
||||
* @param node - To place.
|
||||
* @throws If slot is out of range.
|
||||
*/
|
||||
public setNode(slot: number, node: Node): void {
|
||||
if (slot < 0 || slot > SLOT_MAX) {
|
||||
throw new Error('Invalid slot: slot must be between 0-15.')
|
||||
}
|
||||
this.leaves[slot] = node
|
||||
this.empty = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node that is currently in a given slot.
|
||||
*
|
||||
* @param slot - A number 0-15.
|
||||
* @returns Node currently in a slot.
|
||||
* @throws If slot is out of range.
|
||||
*/
|
||||
public getNode(slot: number): Node | undefined {
|
||||
if (slot < 0 || slot > SLOT_MAX) {
|
||||
throw new Error('Invalid slot: slot must be between 0-15.')
|
||||
}
|
||||
return this.leaves[slot]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of a LeafNode.
|
||||
*
|
||||
* @returns Hash of the LeafNode.
|
||||
*/
|
||||
public get hash(): string {
|
||||
if (this.empty) {
|
||||
return HEX_ZERO
|
||||
}
|
||||
let hex = ''
|
||||
for (let iter = 0; iter <= SLOT_MAX; iter++) {
|
||||
const child = this.leaves[iter]
|
||||
const hash: string = child == null ? HEX_ZERO : child.hash
|
||||
hex += hash
|
||||
}
|
||||
|
||||
const prefix = hashPrefix.INNER_NODE.toString(HEX)
|
||||
return sha512Half(prefix + hex)
|
||||
}
|
||||
}
|
||||
|
||||
export default InnerNode
|
||||
67
src/utils/hashes/shaMap/leafNode.ts
Normal file
67
src/utils/hashes/shaMap/leafNode.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import hashPrefix from '../hashPrefix'
|
||||
import sha512Half from '../sha512Half'
|
||||
|
||||
import { NodeType, Node } from './node'
|
||||
|
||||
const HEX = 16
|
||||
|
||||
/**
|
||||
* Class for SHAMap Leaf Node.
|
||||
*/
|
||||
class Leaf extends Node {
|
||||
public tag: string
|
||||
public type: NodeType
|
||||
public data: string
|
||||
|
||||
/**
|
||||
* Leaf node in a SHAMap tree.
|
||||
*
|
||||
* @param tag - Equates to a ledger entry `index`.
|
||||
* @param data - Hex of account state, transaction etc.
|
||||
* @param type - One of TYPE_ACCOUNT_STATE, TYPE_TRANSACTION_MD etc.
|
||||
*/
|
||||
public constructor(tag: string, data: string, type: NodeType) {
|
||||
super()
|
||||
this.tag = tag
|
||||
this.type = type
|
||||
this.data = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to Leaf.
|
||||
*
|
||||
* @param tag - Index of the Node.
|
||||
* @param node - Node to insert.
|
||||
* @throws When called, because LeafNodes cannot addItem.
|
||||
*/
|
||||
public addItem(tag: string, node: Node): void {
|
||||
throw new Error('Cannot call addItem on a LeafNode')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of a LeafNode.
|
||||
*
|
||||
* @returns Hash or undefined.
|
||||
* @throws If node is of unknown type.
|
||||
*/
|
||||
public get hash(): string {
|
||||
switch (this.type) {
|
||||
case NodeType.ACCOUNT_STATE: {
|
||||
const leafPrefix = hashPrefix.LEAF_NODE.toString(HEX)
|
||||
return sha512Half(leafPrefix + this.data + this.tag)
|
||||
}
|
||||
case NodeType.TRANSACTION_NO_METADATA: {
|
||||
const txIDPrefix = hashPrefix.TRANSACTION_ID.toString(HEX)
|
||||
return sha512Half(txIDPrefix + this.data)
|
||||
}
|
||||
case NodeType.TRANSACTION_METADATA: {
|
||||
const txNodePrefix = hashPrefix.TRANSACTION_NODE.toString(HEX)
|
||||
return sha512Half(txNodePrefix + this.data + this.tag)
|
||||
}
|
||||
default:
|
||||
throw new Error('Tried to hash a SHAMap node of unknown type.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Leaf
|
||||
14
src/utils/hashes/shaMap/node.ts
Normal file
14
src/utils/hashes/shaMap/node.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum NodeType {
|
||||
INNER = 1,
|
||||
TRANSACTION_NO_METADATA = 2,
|
||||
TRANSACTION_METADATA = 3,
|
||||
ACCOUNT_STATE = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for SHAMapNode.
|
||||
*/
|
||||
export abstract class Node {
|
||||
public abstract addItem(_tag: string, _node: Node): void
|
||||
public abstract get hash(): string
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import hashPrefix from './hashPrefix'
|
||||
import sha512Half from './sha512Half'
|
||||
|
||||
const HEX_ZERO =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
export enum NodeType {
|
||||
INNER = 1,
|
||||
TRANSACTION_NO_METADATA = 2,
|
||||
TRANSACTION_METADATA = 3,
|
||||
ACCOUNT_STATE = 4,
|
||||
}
|
||||
|
||||
export abstract class Node {
|
||||
/**
|
||||
* Abstract constructor representing a node in a SHAMap tree.
|
||||
* Can be either InnerNode or Leaf.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public addItem(_tag: string, _node: Node): void {
|
||||
throw new Error(
|
||||
'Called unimplemented virtual method SHAMapTreeNode#addItem.',
|
||||
)
|
||||
}
|
||||
|
||||
public get hash(): string | void {
|
||||
throw new Error('Called unimplemented virtual method SHAMapTreeNode#hash.')
|
||||
}
|
||||
}
|
||||
|
||||
export class InnerNode extends Node {
|
||||
public leaves: { [slot: number]: Node }
|
||||
public type: NodeType
|
||||
public depth: number
|
||||
public empty: boolean
|
||||
|
||||
/**
|
||||
* Define an Inner (non-leaf) node in a SHAMap tree.
|
||||
*
|
||||
* @param depth - I.e. How many parent inner nodes.
|
||||
* @class
|
||||
*/
|
||||
public constructor(depth = 0) {
|
||||
super()
|
||||
this.leaves = {}
|
||||
this.type = NodeType.INNER
|
||||
this.depth = depth
|
||||
this.empty = true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tag - Equates to a ledger entry `index`.
|
||||
* @param {Node} node - To add.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
public addItem(tag: string, node: Node): void {
|
||||
const existingNode = this.getNode(parseInt(tag[this.depth], 16))
|
||||
if (existingNode) {
|
||||
// A node already exists in this slot
|
||||
if (existingNode instanceof InnerNode) {
|
||||
// There is an inner node, so we need to go deeper
|
||||
existingNode.addItem(tag, node)
|
||||
} else if (existingNode instanceof Leaf) {
|
||||
if (existingNode.tag === tag) {
|
||||
// Collision
|
||||
throw new Error(
|
||||
'Tried to add a node to a SHAMap that was already in there.',
|
||||
)
|
||||
} else {
|
||||
// Turn it into an inner node
|
||||
const newInnerNode = new InnerNode(this.depth + 1)
|
||||
|
||||
// Parent new and existing node
|
||||
newInnerNode.addItem(existingNode.tag, existingNode)
|
||||
newInnerNode.addItem(tag, node)
|
||||
|
||||
// And place the newly created inner node in the slot
|
||||
this.setNode(parseInt(tag[this.depth], 16), newInnerNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Neat, we have a nice open spot for the new node
|
||||
this.setNode(parseInt(tag[this.depth], 16), node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the node that is currently in a given slot.
|
||||
*
|
||||
* @param slot - A number 0-15.
|
||||
* @param node - To place.
|
||||
* @returns
|
||||
*/
|
||||
public setNode(slot: number, node: Node): void {
|
||||
if (slot < 0 || slot > 15) {
|
||||
throw new Error('Invalid slot: slot must be between 0-15.')
|
||||
}
|
||||
this.leaves[slot] = node
|
||||
this.empty = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node that is currently in a given slot.
|
||||
*
|
||||
* @param slot - A number 0-15.
|
||||
* @returns
|
||||
*/
|
||||
public getNode(slot: number): Node {
|
||||
if (slot < 0 || slot > 15) {
|
||||
throw new Error('Invalid slot: slot must be between 0-15.')
|
||||
}
|
||||
return this.leaves[slot]
|
||||
}
|
||||
|
||||
public get hash(): string {
|
||||
if (this.empty) {
|
||||
return HEX_ZERO
|
||||
}
|
||||
let hex = ''
|
||||
for (let i = 0; i < 16; i++) {
|
||||
hex += this.leaves[i] ? this.leaves[i].hash : HEX_ZERO
|
||||
}
|
||||
const prefix = hashPrefix.INNER_NODE.toString(16)
|
||||
return sha512Half(prefix + hex)
|
||||
}
|
||||
}
|
||||
|
||||
export class Leaf extends Node {
|
||||
public tag: string
|
||||
public type: NodeType
|
||||
public data: string
|
||||
|
||||
/**
|
||||
* Leaf node in a SHAMap tree.
|
||||
*
|
||||
* @param tag - Equates to a ledger entry `index`.
|
||||
* @param data - Hex of account state, transaction etc.
|
||||
* @param type - One of TYPE_ACCOUNT_STATE, TYPE_TRANSACTION_MD etc.
|
||||
* @class
|
||||
*/
|
||||
public constructor(tag: string, data: string, type: NodeType) {
|
||||
super()
|
||||
this.tag = tag
|
||||
this.type = type
|
||||
this.data = data
|
||||
}
|
||||
|
||||
public get hash(): string | void {
|
||||
switch (this.type) {
|
||||
case NodeType.ACCOUNT_STATE: {
|
||||
const leafPrefix = hashPrefix.LEAF_NODE.toString(16)
|
||||
return sha512Half(leafPrefix + this.data + this.tag)
|
||||
}
|
||||
case NodeType.TRANSACTION_NO_METADATA: {
|
||||
const txIDPrefix = hashPrefix.TRANSACTION_ID.toString(16)
|
||||
return sha512Half(txIDPrefix + this.data)
|
||||
}
|
||||
case NodeType.TRANSACTION_METADATA: {
|
||||
const txNodePrefix = hashPrefix.TRANSACTION_NODE.toString(16)
|
||||
return sha512Half(txNodePrefix + this.data + this.tag)
|
||||
}
|
||||
default:
|
||||
throw new Error('Tried to hash a SHAMap node of unknown type.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SHAMap {
|
||||
public root: InnerNode
|
||||
|
||||
/**
|
||||
* SHAMap tree.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
public constructor() {
|
||||
this.root = new InnerNode(0)
|
||||
}
|
||||
|
||||
public addItem(tag: string, data: string, type: NodeType): void {
|
||||
this.root.addItem(tag, new Leaf(tag, data, type))
|
||||
}
|
||||
|
||||
public get hash(): string {
|
||||
return this.root.hash
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import _ from 'lodash'
|
||||
import { xAddressToClassicAddress } from 'ripple-address-codec'
|
||||
|
||||
import { ValidationError } from '../common/errors'
|
||||
@@ -17,114 +15,37 @@ import {
|
||||
computeTransactionTreeHash,
|
||||
computeStateTreeHash,
|
||||
computeLedgerHash,
|
||||
computeLedgerHeaderHash,
|
||||
computeEscrowHash,
|
||||
computePaymentChannelHash,
|
||||
} from './hashes'
|
||||
import computeLedgerHeaderHash from './ledgerHash'
|
||||
import signPaymentChannelClaim from './signPaymentChannelClaim'
|
||||
import { rippleTimeToISOTime, ISOTimeToRippleTime } from './timeConversion'
|
||||
import verifyPaymentChannelClaim from './verifyPaymentChannelClaim'
|
||||
import { xrpToDrops, dropsToXrp } from './xrpConversion'
|
||||
|
||||
/**
|
||||
* Check if a secret is valid.
|
||||
*
|
||||
* @param secret - Secret to test for validity.
|
||||
* @returns True if secret can be derived into a keypair.
|
||||
*/
|
||||
function isValidSecret(secret: string): boolean {
|
||||
try {
|
||||
deriveKeypair(secret)
|
||||
return true
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function dropsToXrp(drops: BigNumber.Value): string {
|
||||
if (typeof drops === 'string') {
|
||||
if (!/^-?[0-9]*\.?[0-9]*$/.exec(drops)) {
|
||||
throw new ValidationError(
|
||||
`dropsToXrp: invalid value '${drops}',` +
|
||||
` should be a number matching (^-?[0-9]*\\.?[0-9]*$).`,
|
||||
)
|
||||
} else if (drops === '.') {
|
||||
throw new ValidationError(
|
||||
`dropsToXrp: invalid value '${drops}',` +
|
||||
` should be a BigNumber or string-encoded number.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Converting to BigNumber and then back to string should remove any
|
||||
// decimal point followed by zeros, e.g. '1.00'.
|
||||
// Important: specify base 10 to avoid exponential notation, e.g. '1e-7'.
|
||||
drops = new BigNumber(drops).toString(10)
|
||||
|
||||
// drops are only whole units
|
||||
if (drops.includes('.')) {
|
||||
throw new ValidationError(
|
||||
`dropsToXrp: value '${drops}' has` + ` too many decimal places.`,
|
||||
)
|
||||
}
|
||||
|
||||
// This should never happen; the value has already been
|
||||
// validated above. This just ensures BigNumber did not do
|
||||
// something unexpected.
|
||||
if (!/^-?[0-9]+$/.exec(drops)) {
|
||||
throw new ValidationError(
|
||||
`dropsToXrp: failed sanity check -` +
|
||||
` value '${drops}',` +
|
||||
` does not match (^-?[0-9]+$).`,
|
||||
)
|
||||
}
|
||||
|
||||
return new BigNumber(drops).dividedBy(1000000.0).toString(10)
|
||||
}
|
||||
|
||||
function xrpToDrops(xrp: BigNumber.Value): string {
|
||||
if (typeof xrp === 'string') {
|
||||
if (!/^-?[0-9]*\.?[0-9]*$/.exec(xrp)) {
|
||||
throw new ValidationError(
|
||||
`xrpToDrops: invalid value '${xrp}',` +
|
||||
` should be a number matching (^-?[0-9]*\\.?[0-9]*$).`,
|
||||
)
|
||||
} else if (xrp === '.') {
|
||||
throw new ValidationError(
|
||||
`xrpToDrops: invalid value '${xrp}',` +
|
||||
` should be a BigNumber or string-encoded number.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Important: specify base 10 to avoid exponential notation, e.g. '1e-7'.
|
||||
xrp = new BigNumber(xrp).toString(10)
|
||||
|
||||
// This should never happen; the value has already been
|
||||
// validated above. This just ensures BigNumber did not do
|
||||
// something unexpected.
|
||||
if (!/^-?[0-9.]+$/.exec(xrp)) {
|
||||
throw new ValidationError(
|
||||
`xrpToDrops: failed sanity check -` +
|
||||
` value '${xrp}',` +
|
||||
` does not match (^-?[0-9.]+$).`,
|
||||
)
|
||||
}
|
||||
|
||||
const components = xrp.split('.')
|
||||
if (components.length > 2) {
|
||||
throw new ValidationError(
|
||||
`xrpToDrops: failed sanity check -` +
|
||||
` value '${xrp}' has` +
|
||||
` too many decimal points.`,
|
||||
)
|
||||
}
|
||||
|
||||
const fraction = components[1] || '0'
|
||||
if (fraction.length > 6) {
|
||||
throw new ValidationError(
|
||||
`xrpToDrops: value '${xrp}' has` + ` too many decimal places.`,
|
||||
)
|
||||
}
|
||||
|
||||
return new BigNumber(xrp)
|
||||
.times(1000000.0)
|
||||
.integerValue(BigNumber.ROUND_FLOOR)
|
||||
.toString(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Remove/rename this function.
|
||||
*
|
||||
* @param amount - Convert an Amount in.
|
||||
* @returns Amount without X-Address issuer.
|
||||
* @throws When issuer X-Address includes a tag.
|
||||
*/
|
||||
function toRippledAmount(amount: RippledAmount): RippledAmount {
|
||||
if (typeof amount === 'string') {
|
||||
return amount
|
||||
@@ -137,14 +58,14 @@ function toRippledAmount(amount: RippledAmount): RippledAmount {
|
||||
return amount.value
|
||||
}
|
||||
|
||||
let issuer = amount.counterparty || amount.issuer
|
||||
let issuer = amount.counterparty ?? amount.issuer
|
||||
let tag: number | false = false
|
||||
|
||||
try {
|
||||
if (issuer) {
|
||||
;({ classicAddress: issuer, tag } = xAddressToClassicAddress(issuer))
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
/* not an X-address */
|
||||
}
|
||||
|
||||
@@ -159,58 +80,23 @@ function toRippledAmount(amount: RippledAmount): RippledAmount {
|
||||
}
|
||||
}
|
||||
|
||||
function convertKeysFromSnakeCaseToCamelCase(obj: any): any {
|
||||
if (typeof obj === 'object') {
|
||||
const accumulator = Array.isArray(obj) ? [] : {}
|
||||
let newKey
|
||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
||||
newKey = key
|
||||
// taking this out of function leads to error in PhantomJS
|
||||
const FINDSNAKE = /([a-zA-Z]_[a-zA-Z])/g
|
||||
if (FINDSNAKE.test(key)) {
|
||||
newKey = key.replace(FINDSNAKE, (r) => r[0] + r[2].toUpperCase())
|
||||
}
|
||||
result[newKey] = convertKeysFromSnakeCaseToCamelCase(value)
|
||||
return result
|
||||
}, accumulator)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function removeUndefined<T extends object>(obj: T): T {
|
||||
return _.omitBy(obj, (value) => value == null) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rpepoch - (seconds since 1/1/2000 GMT).
|
||||
* @returns Milliseconds since unix epoch.
|
||||
* Removes undefined values from an object.
|
||||
*
|
||||
* @param obj - Object to remove undefined values from.
|
||||
* @returns The same object, but without undefined values.
|
||||
*/
|
||||
function rippleToUnixTimestamp(rpepoch: number): number {
|
||||
return (rpepoch + 0x386d4380) * 1000
|
||||
}
|
||||
function removeUndefined<T extends Record<string, unknown>>(obj: T): T {
|
||||
const newObj = { ...obj }
|
||||
|
||||
/**
|
||||
* @param timestamp - (ms since unix epoch).
|
||||
* @returns Seconds since Ripple Epoch (1/1/2000 GMT).
|
||||
*/
|
||||
function unixToRippleTimestamp(timestamp: number): number {
|
||||
return Math.round(timestamp / 1000) - 0x386d4380
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Deletes undefined values. */
|
||||
delete newObj[key]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param rippleTime - Is the number of seconds since Ripple Epoch (1/1/2000 GMT).
|
||||
* @returns Iso8601 international standard date format.
|
||||
*/
|
||||
function rippleTimeToISOTime(rippleTime: number): string {
|
||||
return new Date(rippleToUnixTimestamp(rippleTime)).toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iso8601 - International standard date format.
|
||||
* @returns Seconds since ripple epoch (1/1/2000 GMT).
|
||||
*/
|
||||
function ISOTimeToRippleTime(iso8601: string): number {
|
||||
return unixToRippleTimestamp(Date.parse(iso8601))
|
||||
return newObj
|
||||
}
|
||||
|
||||
function convertStringToHex(string: string): string {
|
||||
@@ -218,11 +104,9 @@ function convertStringToHex(string: string): string {
|
||||
}
|
||||
|
||||
export {
|
||||
computeLedgerHeaderHash,
|
||||
dropsToXrp,
|
||||
xrpToDrops,
|
||||
toRippledAmount,
|
||||
convertKeysFromSnakeCaseToCamelCase,
|
||||
removeUndefined,
|
||||
rippleTimeToISOTime,
|
||||
ISOTimeToRippleTime,
|
||||
@@ -236,6 +120,7 @@ export {
|
||||
computeTransactionTreeHash,
|
||||
computeStateTreeHash,
|
||||
computeLedgerHash,
|
||||
computeLedgerHeaderHash,
|
||||
computeEscrowHash,
|
||||
computePaymentChannelHash,
|
||||
generateXAddress,
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import { ValidationError } from '../common/errors'
|
||||
|
||||
import {
|
||||
computeLedgerHash,
|
||||
computeTransactionTreeHash,
|
||||
computeStateTreeHash,
|
||||
} from './hashes'
|
||||
|
||||
import { ISOTimeToRippleTime } from '.'
|
||||
|
||||
function convertLedgerHeader(header): any {
|
||||
return {
|
||||
account_hash: header.stateHash,
|
||||
close_time: ISOTimeToRippleTime(header.closeTime),
|
||||
close_time_resolution: header.closeTimeResolution,
|
||||
close_flags: header.closeFlags,
|
||||
hash: header.ledgerHash,
|
||||
ledger_hash: header.ledgerHash,
|
||||
ledger_index: header.ledgerVersion.toString(),
|
||||
parent_hash: header.parentLedgerHash,
|
||||
parent_close_time: ISOTimeToRippleTime(header.parentCloseTime),
|
||||
total_coins: header.totalDrops,
|
||||
transaction_hash: header.transactionHash,
|
||||
}
|
||||
}
|
||||
|
||||
function hashLedgerHeader(ledgerHeader) {
|
||||
const header = convertLedgerHeader(ledgerHeader)
|
||||
return computeLedgerHash(header)
|
||||
}
|
||||
|
||||
function computeLedgerTransactionHash(
|
||||
ledger,
|
||||
options: ComputeLedgerHeaderHashOptions,
|
||||
) {
|
||||
let transactions: any[] = []
|
||||
if (ledger.rawTransactions) {
|
||||
transactions = JSON.parse(ledger.rawTransactions)
|
||||
} else if (ledger.transactions) {
|
||||
try {
|
||||
transactions = ledger.transactions.map((tx) =>
|
||||
JSON.parse(tx.rawTransaction),
|
||||
)
|
||||
} catch (e) {
|
||||
if (
|
||||
e.toString() ===
|
||||
'SyntaxError: Unexpected' + ' token u in JSON at position 0'
|
||||
) {
|
||||
// one or more of the `tx.rawTransaction`s is undefined
|
||||
throw new ValidationError('ledger' + ' is missing raw transactions')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.computeTreeHashes) {
|
||||
throw new ValidationError(
|
||||
'transactions' + ' property is missing from the ledger',
|
||||
)
|
||||
}
|
||||
return ledger.transactionHash
|
||||
}
|
||||
const txs = transactions.map((tx) => {
|
||||
const mergeTx = { ..._.omit(tx, 'tx'), ...(tx.tx || {}) }
|
||||
// rename `meta` back to `metaData`
|
||||
const renameMeta = {
|
||||
..._.omit(mergeTx, 'meta'),
|
||||
...(tx.meta ? { metaData: tx.meta } : {}),
|
||||
}
|
||||
return renameMeta
|
||||
})
|
||||
const transactionHash = computeTransactionTreeHash(txs)
|
||||
if (
|
||||
ledger.transactionHash != null &&
|
||||
ledger.transactionHash !== transactionHash
|
||||
) {
|
||||
throw new ValidationError(
|
||||
'transactionHash in header' +
|
||||
' does not match computed hash of transactions',
|
||||
{
|
||||
transactionHashInHeader: ledger.transactionHash,
|
||||
computedHashOfTransactions: transactionHash,
|
||||
},
|
||||
)
|
||||
}
|
||||
return transactionHash
|
||||
}
|
||||
|
||||
function computeLedgerStateHash(
|
||||
ledger,
|
||||
options: ComputeLedgerHeaderHashOptions,
|
||||
) {
|
||||
if (ledger.rawState == null) {
|
||||
if (options.computeTreeHashes) {
|
||||
throw new ValidationError(
|
||||
'rawState' + ' property is missing from the ledger',
|
||||
)
|
||||
}
|
||||
return ledger.stateHash
|
||||
}
|
||||
const state = JSON.parse(ledger.rawState)
|
||||
const stateHash = computeStateTreeHash(state)
|
||||
if (ledger.stateHash != null && ledger.stateHash !== stateHash) {
|
||||
throw new ValidationError(
|
||||
'stateHash in header' + ' does not match computed hash of state',
|
||||
)
|
||||
}
|
||||
return stateHash
|
||||
}
|
||||
|
||||
export interface ComputeLedgerHeaderHashOptions {
|
||||
computeTreeHashes?: boolean
|
||||
}
|
||||
|
||||
function computeLedgerHeaderHash(
|
||||
ledger: any,
|
||||
options: ComputeLedgerHeaderHashOptions = {},
|
||||
): string {
|
||||
const subhashes = {
|
||||
transactionHash: computeLedgerTransactionHash(ledger, options),
|
||||
stateHash: computeLedgerStateHash(ledger, options),
|
||||
}
|
||||
return hashLedgerHeader({ ...ledger, ...subhashes })
|
||||
}
|
||||
|
||||
export default computeLedgerHeaderHash
|
||||
@@ -1,8 +1,16 @@
|
||||
import binary from 'ripple-binary-codec'
|
||||
import keypairs from 'ripple-keypairs'
|
||||
|
||||
import { xrpToDrops } from '.'
|
||||
import { xrpToDrops } from './xrpConversion'
|
||||
|
||||
/**
|
||||
* Sign a payment channel claim.
|
||||
*
|
||||
* @param channel - Channel identifier specified by the paymentChannelClaim.
|
||||
* @param amount - Amount specified by the paymentChannelClaim.
|
||||
* @param privateKey - Private Key to sign paymentChannelClaim with.
|
||||
* @returns True if the channel is valid.
|
||||
*/
|
||||
function signPaymentChannelClaim(
|
||||
channel: string,
|
||||
amount: string,
|
||||
|
||||
46
src/utils/timeConversion.ts
Normal file
46
src/utils/timeConversion.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
const RIPPLE_EPOCH_DIFF = 0x386d4380
|
||||
|
||||
/**
|
||||
* Convert a ripple timestamp to a unix timestamp.
|
||||
*
|
||||
* @param rpepoch - (seconds since 1/1/2000 GMT).
|
||||
* @returns Milliseconds since unix epoch.
|
||||
*/
|
||||
function rippleToUnixTimestamp(rpepoch: number): number {
|
||||
return (rpepoch + RIPPLE_EPOCH_DIFF) * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a unix timestamp to a ripple timestamp.
|
||||
*
|
||||
* @param timestamp - (ms since unix epoch).
|
||||
* @returns Seconds since Ripple Epoch (1/1/2000 GMT).
|
||||
*/
|
||||
function unixToRippleTimestamp(timestamp: number): number {
|
||||
return Math.round(timestamp / 1000) - RIPPLE_EPOCH_DIFF
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a ripple timestamp to an Iso8601 timestamp.
|
||||
*
|
||||
* @param rippleTime - Is the number of seconds since Ripple Epoch (1/1/2000 GMT).
|
||||
* @returns Iso8601 international standard date format.
|
||||
*/
|
||||
function rippleTimeToISOTime(rippleTime: number): string {
|
||||
return new Date(rippleToUnixTimestamp(rippleTime)).toISOString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an Iso8601 timestmap to a ripple timestamp.
|
||||
*
|
||||
* @param iso8601 - International standard date format.
|
||||
* @returns Seconds since ripple epoch (1/1/2000 GMT).
|
||||
*/
|
||||
function ISOTimeToRippleTime(iso8601: string): number {
|
||||
return unixToRippleTimestamp(Date.parse(iso8601))
|
||||
}
|
||||
|
||||
export {
|
||||
rippleTimeToISOTime,
|
||||
ISOTimeToRippleTime,
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import binary from 'ripple-binary-codec'
|
||||
import keypairs from 'ripple-keypairs'
|
||||
|
||||
import { xrpToDrops } from '.'
|
||||
import { xrpToDrops } from './xrpConversion'
|
||||
|
||||
/**
|
||||
* Verify the signature of a payment channel claim.
|
||||
*
|
||||
* @param channel - Channel identifier specified by the paymentChannelClaim.
|
||||
* @param amount - Amount specified by the paymentChannelClaim.
|
||||
* @param signature - Signature produced from signing paymentChannelClaim.
|
||||
* @param publicKey - Public key that signed the paymentChannelClaim.
|
||||
* @returns True if the channel is valid.
|
||||
*/
|
||||
function verifyPaymentChannelClaim(
|
||||
channel: string,
|
||||
amount: string,
|
||||
|
||||
22
test/fixtures/requests/computeLedgerHash.json
vendored
22
test/fixtures/requests/computeLedgerHash.json
vendored
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"stateHash": "D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44",
|
||||
"closeTime": "2015-08-12T01:01:10.000Z",
|
||||
"parentCloseTime": "2015-08-12T01:01:00.000Z",
|
||||
"closeFlags": 0,
|
||||
"closeTimeResolution": 10,
|
||||
"ledgerVersion": 15202439,
|
||||
"parentLedgerHash": "12724A65B030C15A1573AA28B1BBB5DF3DA4589AA3623675A31CAE69B23B1C4E",
|
||||
"totalDrops": "99998831688050493",
|
||||
"transactionHash": "325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C8"
|
||||
"account_hash": "D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44",
|
||||
"close_time": 492656470,
|
||||
"parent_close_time": 492656460,
|
||||
"close_flags": 0,
|
||||
"ledger_index": "15202439",
|
||||
"close_time_human": "2015-Aug-12 01:01:10.000000000 UTC",
|
||||
"close_time_resolution": 10,
|
||||
"closed": true,
|
||||
"hash": "F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349",
|
||||
"ledger_hash": "F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349",
|
||||
"parent_hash": "12724A65B030C15A1573AA28B1BBB5DF3DA4589AA3623675A31CAE69B23B1C4E",
|
||||
"total_coins": "99998831688050493",
|
||||
"transaction_hash": "325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C8"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
[
|
||||
{
|
||||
"hash": "F8F337DEE5D5B238A10AF4A4D56926BA26C83EE7AF5A5A6474340C56F9252DF3",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "Payment",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 1608,
|
||||
@@ -13,9 +9,8 @@
|
||||
"SigningPubKey": "03BC0973F997BC6384BE455B163519A3E96BC2D725C37F7172D5FED5DD38E2A357",
|
||||
"TxnSignature": "3045022100D80A1802B00AEEF9FDFDE594B0D568217A312D54E6337B8519C0D699841EFB96022067F6913B13D0EC2354C5A67CE0A41AE4181A09CD08A1BB0638D128D357961006",
|
||||
"Account": "rDPL68aNpdfp9h59R4QT5R6B1Z2W9oRc51",
|
||||
"Destination": "rE4S4Xw8euysJ3mt7gmK8EhhYEwmALpb3R"
|
||||
},
|
||||
"meta": {
|
||||
"Destination": "rE4S4Xw8euysJ3mt7gmK8EhhYEwmALpb3R",
|
||||
"metaData": {
|
||||
"TransactionIndex": 6,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -60,10 +55,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "F8D5DE632B1D8B64E577C46912CCE483D6DF4FD4E2CF4A3D586A099DE3B27021",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "Payment",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 18874,
|
||||
@@ -73,9 +64,8 @@
|
||||
"SigningPubKey": "035D097E75D4B35345CEB30F9B1D18CB81165FE6ADD02481AA5B02B5F9C8107EE1",
|
||||
"TxnSignature": "304402203D80E8BC71908AB345948AB71FB7B8DE239DD79636D96D3C5BDA2B2F192A5EEA0220686413D69BF0D813FC61DABD437AEFAAE69925D3E10FCD5B2C4D90B5AF7B883D",
|
||||
"Account": "rnHScgV6wSP9sR25uYWiMo3QYNA5ybQ7cH",
|
||||
"Destination": "rwnnfHDaEAwXaVji52cWWizbHVMs2Cz5K9"
|
||||
},
|
||||
"meta": {
|
||||
"Destination": "rwnnfHDaEAwXaVji52cWWizbHVMs2Cz5K9",
|
||||
"metaData": {
|
||||
"TransactionIndex": 5,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -120,10 +110,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "E9004490A92413E92DACD621AC73FD434A8950C350F7572FFEAF4D6AAF8FC288",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "Payment",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 1615,
|
||||
@@ -133,9 +119,8 @@
|
||||
"SigningPubKey": "03ACFAA11628C558AB5E7FA64705F442BDAABA6E9D318B30E010BC87CDEA8D1D7D",
|
||||
"TxnSignature": "3045022100A3530C2E983FB05DFF27172C649494291F7BEBA2E6A59EEAF945CB9728D1DB5E022015BCA0E9D69760224DD7C2B68F3BC1F239D89C3397161AA3901C2E04EE31C18F",
|
||||
"Account": "razcSDpwds1aTeqDphqzBr7ay1ZELYAWTm",
|
||||
"Destination": "rhuqJAE2UfhGCvkR7Ve35bvm39JmRvFML4"
|
||||
},
|
||||
"meta": {
|
||||
"Destination": "rhuqJAE2UfhGCvkR7Ve35bvm39JmRvFML4",
|
||||
"metaData": {
|
||||
"TransactionIndex": 4,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -180,10 +165,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "D44BFF924D23211B82B8F604AF6D92F260F8DD13103A96F03E48825C4A978FD6",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "Payment",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 1674,
|
||||
@@ -193,9 +174,8 @@
|
||||
"SigningPubKey": "028F28D78FDA74222F4008F012247DF3BBD42B90CE4CFD87E29598196108E91B52",
|
||||
"TxnSignature": "3044022065A003194D91E774D180BE47D4E086BB2624BC8F6DB7C655E135D5C6C03BBC7C02205DC961C2B7A06D701B29C2116ACF6F84CC84205FF44411576C15507852ECC31C",
|
||||
"Account": "rQGLp9nChtWkdgcHjj6McvJithN2S2HJsP",
|
||||
"Destination": "rEUubanepAAugnNJY1gxEZLDnk9W5NCoFU"
|
||||
},
|
||||
"meta": {
|
||||
"Destination": "rEUubanepAAugnNJY1gxEZLDnk9W5NCoFU",
|
||||
"metaData": {
|
||||
"TransactionIndex": 3,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -240,10 +220,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "C978D915BFB17687335CBFC4B207D9E7213BCEE35B468C2EEE016CDCE4EDB6E4",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "OfferCreate",
|
||||
"Sequence": 289444,
|
||||
"OfferSequence": 289443,
|
||||
@@ -270,9 +246,8 @@
|
||||
"parsed_memo_type": "offer_comment"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
],
|
||||
"metaData": {
|
||||
"TransactionIndex": 2,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -372,10 +347,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "31B34FD7C90CDC6CF680A814DEBC6F616C69275C0E99711F904DE088A8ED4B28",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "AccountSet",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 387262,
|
||||
@@ -383,9 +354,8 @@
|
||||
"Fee": "10500",
|
||||
"SigningPubKey": "027DFE042DC2BD07D2E88DD526A5FBF816C831C25CA0BB62A3BF320A3B2BA6DB5C",
|
||||
"TxnSignature": "30440220572D89688D9F9DB9874CDDDD3EBDCB5808A836982864C81F185FBC54FAD1A7B902202E09AAA6D65EECC9ACDEA7F70D8D2EE024152C7B288FA9E42C427260CF922F58",
|
||||
"Account": "rn6uAt46Xi6uxA2dRCtqaJyM3aaP6V9WWM"
|
||||
},
|
||||
"meta": {
|
||||
"Account": "rn6uAt46Xi6uxA2dRCtqaJyM3aaP6V9WWM",
|
||||
"metaData": {
|
||||
"TransactionIndex": 1,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
@@ -412,10 +382,6 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"hash": "260BC2964FFE6D81CB25C152F8054FFB2CE6ED04FF89D8D0D0559BC14BEF0E46",
|
||||
"date": "2015-08-12T01:01:10+00:00",
|
||||
"ledger_index": 15202439,
|
||||
"tx": {
|
||||
"TransactionType": "Payment",
|
||||
"Flags": 2147483648,
|
||||
"Sequence": 1673,
|
||||
@@ -425,9 +391,8 @@
|
||||
"SigningPubKey": "02C26CF5D395A1CB352BE10D5AAB73FE27FC0AFAE0BD6121E55D097EBDCF394E11",
|
||||
"TxnSignature": "304402204190B6DC7D14B1CC8DDAA87F1C01FEDA6D67D598D65E1AA19D4ADE937ED14B720220662EE404438F415AD3335B9FBA1A4C2A5F72AA387740D8A011A8C53346481B1D",
|
||||
"Account": "rEE77T1E5vEFcEB9zM92jBD3rPs3kPdS1j",
|
||||
"Destination": "r3AsrDRMNYaKNCofo9a5Us7R66RAzTigiU"
|
||||
},
|
||||
"meta": {
|
||||
"Destination": "r3AsrDRMNYaKNCofo9a5Us7R66RAzTigiU",
|
||||
"metaData": {
|
||||
"TransactionIndex": 0,
|
||||
"AffectedNodes": [
|
||||
{
|
||||
|
||||
2
test/fixtures/requests/index.ts
vendored
2
test/fixtures/requests/index.ts
vendored
@@ -178,7 +178,7 @@ const getOrderbook = {
|
||||
}
|
||||
|
||||
const computeLedgerHash = {
|
||||
header: { ...header, rawTransactions: JSON.stringify(transactions) },
|
||||
header,
|
||||
transactions,
|
||||
}
|
||||
|
||||
|
||||
1
test/fixtures/responses/generateAddress.json
vendored
1
test/fixtures/responses/generateAddress.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"xAddress": "XVLcsWWNiFdUEqoDmSwgxh1abfddG1LtbGFk7omPgYpbyE8",
|
||||
"classicAddress": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
|
||||
"address": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
|
||||
"secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
|
||||
}
|
||||
|
||||
20
test/fixtures/responses/getLedger.json
vendored
20
test/fixtures/responses/getLedger.json
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"stateHash": "EC028EC32896D537ECCA18D18BEBE6AE99709FEFF9EF72DBD3A7819E918D8B96",
|
||||
"closeTime": "2014-09-24T21:21:50.000Z",
|
||||
"closeTimeResolution": 10,
|
||||
"closeFlags": 0,
|
||||
"ledgerHash": "0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F",
|
||||
"ledgerVersion": 9038214,
|
||||
"parentLedgerHash": "4BB9CBE44C39DC67A1BE849C7467FE1A6D1F73949EA163C38A0121A15E04FFDE",
|
||||
"parentCloseTime": "2014-09-24T21:21:40.000Z",
|
||||
"totalDrops": "99999973964317514",
|
||||
"transactionHash": "ECB730839EB55B1B114D5D1AD2CD9A932C35BA9AB6D3A8C2F08935EAC2BAC239"
|
||||
"account_hash": "EC028EC32896D537ECCA18D18BEBE6AE99709FEFF9EF72DBD3A7819E918D8B96",
|
||||
"close_time": "2014-09-24T21:21:50.000Z",
|
||||
"close_time_resolution": 10,
|
||||
"close_flags": 0,
|
||||
"ledger_hash": "0F7ED9F40742D8A513AE86029462B7A6768325583DF8EE21B7EC663019DD6A0F",
|
||||
"ledger_index": 9038214,
|
||||
"parent_hash": "4BB9CBE44C39DC67A1BE849C7467FE1A6D1F73949EA163C38A0121A15E04FFDE",
|
||||
"parent_close_time": "2014-09-24T21:21:40.000Z",
|
||||
"total_coins": "99999973964317514",
|
||||
"transaction_hash": "ECB730839EB55B1B114D5D1AD2CD9A932C35BA9AB6D3A8C2F08935EAC2BAC239"
|
||||
}
|
||||
|
||||
23
test/fixtures/responses/getLedgerFull.json
vendored
23
test/fixtures/responses/getLedgerFull.json
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import { SHAMap, NodeType } from '../src/utils/hashes/shamap'
|
||||
import SHAMap, { NodeType } from '../src/utils/hashes/shaMap'
|
||||
|
||||
const TYPE_TRANSACTION_NO_METADATA = NodeType.TRANSACTION_NO_METADATA
|
||||
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { ValidationError } from '../../src/common/errors'
|
||||
import { computeLedgerHeaderHash } from '../../src/utils'
|
||||
import { computeLedgerHash } from '../../src/utils'
|
||||
import requests from '../fixtures/requests'
|
||||
import responses from '../fixtures/responses'
|
||||
import { assertResultMatch } from '../testUtils'
|
||||
|
||||
const { computeLedgerHash: REQUEST_FIXTURES } = requests
|
||||
|
||||
describe('computeLedgerHash', function () {
|
||||
let ledger
|
||||
beforeEach(function () {
|
||||
ledger = JSON.parse(JSON.stringify(responses.getLedger.full))
|
||||
|
||||
if (ledger.rawState != null) {
|
||||
ledger.accountState = JSON.parse(ledger.rawState)
|
||||
}
|
||||
})
|
||||
|
||||
it('given corrupt data - should fail', function () {
|
||||
const ledger = JSON.parse(JSON.stringify(responses.getLedger.full))
|
||||
ledger.transactions[0].rawTransaction =
|
||||
'{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"12000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}'
|
||||
ledger.parentCloseTime = ledger.closeTime
|
||||
let hash: string
|
||||
ledger.transactions[0] = JSON.parse(
|
||||
'{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"12000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","metaData":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}',
|
||||
)
|
||||
|
||||
ledger.parent_close_time = ledger.close_time
|
||||
let hash
|
||||
try {
|
||||
hash = computeLedgerHeaderHash(ledger, { computeTreeHashes: true })
|
||||
hash = computeLedgerHash(ledger, { computeTreeHashes: true })
|
||||
} catch (error) {
|
||||
assert(error instanceof ValidationError)
|
||||
if (error instanceof ValidationError) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'transactionHash in header does not match computed hash of transactions',
|
||||
@@ -29,6 +39,7 @@ describe('computeLedgerHash', function () {
|
||||
computedHashOfTransactions:
|
||||
'EAA1ADF4D627339450F0E95EA88B7069186DD64230BAEBDCF3EEC4D616A9FC68',
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
assert(
|
||||
@@ -38,77 +49,68 @@ describe('computeLedgerHash', function () {
|
||||
})
|
||||
|
||||
it('given ledger without raw transactions - should throw', function () {
|
||||
const ledger = JSON.parse(JSON.stringify(responses.getLedger.full))
|
||||
delete ledger.transactions[0].rawTransaction
|
||||
delete ledger.transactions
|
||||
ledger.parentCloseTime = ledger.closeTime
|
||||
let hash: string
|
||||
try {
|
||||
hash = computeLedgerHeaderHash(ledger, { computeTreeHashes: true })
|
||||
} catch (error) {
|
||||
assert(error instanceof ValidationError)
|
||||
assert.strictEqual(error.message, 'ledger is missing raw transactions')
|
||||
return
|
||||
}
|
||||
assert(
|
||||
false,
|
||||
`Should throw ValidationError instead of producing hash: ${hash}`,
|
||||
|
||||
assert.throws(
|
||||
() => computeLedgerHash(ledger, { computeTreeHashes: true }),
|
||||
ValidationError,
|
||||
'transactions is missing from the ledger',
|
||||
)
|
||||
})
|
||||
|
||||
it('given ledger without state or transactions - only compute ledger hash', function () {
|
||||
const ledger = JSON.parse(JSON.stringify(responses.getLedger.full))
|
||||
assert.strictEqual(
|
||||
ledger.transactions[0].rawTransaction,
|
||||
'{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"10000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","meta":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}',
|
||||
ledger.transactions[0] = JSON.parse(
|
||||
'{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Amount":"10000000000","Destination":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Fee":"10","Flags":0,"Sequence":62,"SigningPubKey":"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E","TransactionType":"Payment","TxnSignature":"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639","hash":"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF","metaData":{"AffectedNodes":[{"CreatedNode":{"LedgerEntryType":"AccountRoot","LedgerIndex":"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E","NewFields":{"Account":"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj","Balance":"10000000000","Sequence":1}}},{"ModifiedNode":{"FinalFields":{"Account":"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV","Balance":"981481999380","Flags":0,"OwnerCount":0,"Sequence":63},"LedgerEntryType":"AccountRoot","LedgerIndex":"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A","PreviousFields":{"Balance":"991481999390","Sequence":62},"PreviousTxnID":"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F","PreviousTxnLgrSeq":31317}}],"TransactionIndex":0,"TransactionResult":"tesSUCCESS"},"ledger_index":38129}',
|
||||
)
|
||||
ledger.parentCloseTime = ledger.closeTime
|
||||
const computeLedgerHash = computeLedgerHeaderHash
|
||||
function testCompute(ledgerToCompute, expectedError): void {
|
||||
let hash = computeLedgerHash(ledgerToCompute)
|
||||
|
||||
ledger.parent_close_time = ledger.close_time
|
||||
|
||||
function testCompute(ledgerToTest, expectedError): void {
|
||||
const hash = computeLedgerHash(ledgerToTest)
|
||||
assert.strictEqual(
|
||||
hash,
|
||||
'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E',
|
||||
)
|
||||
|
||||
// fail if required to compute tree hashes
|
||||
try {
|
||||
hash = computeLedgerHash(ledgerToCompute, { computeTreeHashes: true })
|
||||
} catch (error) {
|
||||
assert(error instanceof ValidationError)
|
||||
assert.strictEqual(error.message, expectedError)
|
||||
return
|
||||
}
|
||||
assert(
|
||||
false,
|
||||
`Should throw ValidationError instead of producing hash: ${hash}`,
|
||||
assert.throws(
|
||||
() => computeLedgerHash(ledgerToTest, { computeTreeHashes: true }),
|
||||
ValidationError,
|
||||
expectedError,
|
||||
)
|
||||
}
|
||||
|
||||
const transactions = ledger.transactions
|
||||
delete ledger.transactions
|
||||
testCompute(ledger, 'transactions property is missing from the ledger')
|
||||
delete ledger.rawState
|
||||
testCompute(ledger, 'transactions property is missing from the ledger')
|
||||
testCompute(ledger, 'transactions is missing from the ledger')
|
||||
delete ledger.accountState
|
||||
testCompute(ledger, 'transactions is missing from the ledger')
|
||||
ledger.transactions = transactions
|
||||
testCompute(ledger, 'rawState property is missing from the ledger')
|
||||
testCompute(ledger, 'accountState is missing from the ledger')
|
||||
})
|
||||
|
||||
it('wrong hash', function () {
|
||||
const ledger = JSON.parse(JSON.stringify(responses.getLedger.full))
|
||||
assertResultMatch(ledger, responses.getLedger.full, 'getLedger')
|
||||
const newLedger = {
|
||||
...ledger,
|
||||
parentCloseTime: ledger.closeTime,
|
||||
stateHash:
|
||||
parent_close_time: ledger.close_time,
|
||||
account_hash:
|
||||
'D9ABF622DA26EEEE48203085D4BC23B0F77DC6F8724AC33D975DA3CA492D2E44',
|
||||
}
|
||||
assert.throws(() => {
|
||||
computeLedgerHeaderHash(newLedger)
|
||||
}, /does not match computed hash of state/u)
|
||||
|
||||
assert.throws(
|
||||
() => {
|
||||
computeLedgerHash(newLedger, { computeTreeHashes: true })
|
||||
},
|
||||
ValidationError,
|
||||
'does not match computed hash of state',
|
||||
)
|
||||
})
|
||||
|
||||
it('computeLedgerHash', function () {
|
||||
const header = REQUEST_FIXTURES.header
|
||||
const ledgerHash = computeLedgerHeaderHash(header)
|
||||
const ledgerHash = computeLedgerHash(header)
|
||||
|
||||
assert.strictEqual(
|
||||
ledgerHash,
|
||||
'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349',
|
||||
@@ -119,21 +121,27 @@ describe('computeLedgerHash', function () {
|
||||
const header = {
|
||||
...REQUEST_FIXTURES.header,
|
||||
transactionHash: undefined,
|
||||
rawTransactions: JSON.stringify(REQUEST_FIXTURES.transactions),
|
||||
rawTransactions: REQUEST_FIXTURES.transactions,
|
||||
}
|
||||
const ledgerHash = computeLedgerHeaderHash(header)
|
||||
const ledgerHash = computeLedgerHash(header)
|
||||
assert.strictEqual(
|
||||
ledgerHash,
|
||||
'F4D865D83EB88C1A1911B9E90641919A1314F36E1B099F8E95FE3B7C77BE3349',
|
||||
)
|
||||
})
|
||||
|
||||
it('computeLedgerHash - incorrent transaction_hash', function () {
|
||||
it('computeLedgerHash - incorrect transaction_hash', function () {
|
||||
const header = {
|
||||
...REQUEST_FIXTURES.header,
|
||||
transactionHash:
|
||||
transaction_hash:
|
||||
'325EACC5271322539EEEC2D6A5292471EF1B3E72AE7180533EFC3B8F0AD435C9',
|
||||
transactions: REQUEST_FIXTURES.transactions as any,
|
||||
}
|
||||
assert.throws(() => computeLedgerHeaderHash(header))
|
||||
|
||||
assert.throws(
|
||||
() => computeLedgerHash(header, { computeTreeHashes: true }),
|
||||
ValidationError,
|
||||
'transactionHash in header does not match computed hash of transactions',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,7 +168,6 @@ describe('generateAddress', function () {
|
||||
|
||||
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
|
||||
classicAddress: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
|
||||
address: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -211,7 +210,6 @@ describe('generateAddress', function () {
|
||||
xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo',
|
||||
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
|
||||
classicAddress: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
|
||||
address: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user