mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-27 07:35:52 +00:00
xahau-patch
This commit is contained in:
5
packages/xahau-binary-codec/src/README.md
Normal file
5
packages/xahau-binary-codec/src/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# ripple-binary-codec
|
||||
|
||||
Serialize and deserialize transactions according to the XRP Ledger protocol.
|
||||
|
||||
If you'd like to add a new transaction, data type, or generally modify this library, please read the [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
194
packages/xahau-binary-codec/src/binary.ts
Normal file
194
packages/xahau-binary-codec/src/binary.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/* eslint-disable func-style */
|
||||
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
import { coreTypes } from './types'
|
||||
import { BinaryParser } from './serdes/binary-parser'
|
||||
import { AccountID } from './types/account-id'
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
import { BinarySerializer, BytesList } from './serdes/binary-serializer'
|
||||
import { sha512Half, transactionID } from './hashes'
|
||||
import {
|
||||
type XrplDefinitionsBase,
|
||||
DEFAULT_DEFINITIONS,
|
||||
type FieldInstance,
|
||||
} from './enums'
|
||||
import { STObject } from './types/st-object'
|
||||
import { JsonObject } from './types/serialized-type'
|
||||
|
||||
/**
|
||||
* Construct a BinaryParser
|
||||
*
|
||||
* @param bytes hex-string or Uint8Array to construct BinaryParser from
|
||||
* @param definitions xahaud definitions used to parse the values of transaction types and such.
|
||||
* Can be customized for sidechains and amendments.
|
||||
* @returns BinaryParser
|
||||
*/
|
||||
const makeParser = (
|
||||
bytes: string | Uint8Array,
|
||||
definitions?: XrplDefinitionsBase,
|
||||
): BinaryParser =>
|
||||
new BinaryParser(
|
||||
bytes instanceof Uint8Array ? bytesToHex(bytes) : bytes,
|
||||
definitions,
|
||||
)
|
||||
|
||||
/**
|
||||
* Parse BinaryParser into JSON
|
||||
*
|
||||
* @param parser BinaryParser object
|
||||
* @param definitions xahaud definitions used to parse the values of transaction types and such.
|
||||
* Can be customized for sidechains and amendments.
|
||||
* @returns JSON for the bytes in the BinaryParser
|
||||
*/
|
||||
const readJSON = (
|
||||
parser: BinaryParser,
|
||||
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
|
||||
): JsonObject =>
|
||||
(parser.readType(coreTypes.STObject) as STObject).toJSON(definitions)
|
||||
|
||||
/**
|
||||
* Parse a hex-string into its JSON interpretation
|
||||
*
|
||||
* @param bytes hex-string to parse into JSON
|
||||
* @param definitions xahaud definitions used to parse the values of transaction types and such.
|
||||
* Can be customized for sidechains and amendments.
|
||||
* @returns JSON
|
||||
*/
|
||||
const binaryToJSON = (
|
||||
bytes: string,
|
||||
definitions?: XrplDefinitionsBase,
|
||||
): JsonObject => readJSON(makeParser(bytes, definitions), definitions)
|
||||
|
||||
/**
|
||||
* Interface for passing parameters to SerializeObject
|
||||
*
|
||||
* @field set signingFieldOnly to true if you want to serialize only signing fields
|
||||
*/
|
||||
interface OptionObject {
|
||||
prefix?: Uint8Array
|
||||
suffix?: Uint8Array
|
||||
signingFieldsOnly?: boolean
|
||||
definitions?: XrplDefinitionsBase
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to serialize JSON object representing a transaction
|
||||
*
|
||||
* @param object JSON object to serialize
|
||||
* @param opts options for serializing, including optional prefix, suffix, signingFieldOnly, and definitions
|
||||
* @returns A Uint8Array containing the serialized object
|
||||
*/
|
||||
function serializeObject(
|
||||
object: JsonObject,
|
||||
opts: OptionObject = {},
|
||||
): Uint8Array {
|
||||
const { prefix, suffix, signingFieldsOnly = false, definitions } = opts
|
||||
const bytesList = new BytesList()
|
||||
|
||||
if (prefix) {
|
||||
bytesList.put(prefix)
|
||||
}
|
||||
|
||||
const filter = signingFieldsOnly
|
||||
? (f: FieldInstance): boolean => f.isSigningField
|
||||
: undefined
|
||||
;(coreTypes.STObject as typeof STObject)
|
||||
.from(object, filter, definitions)
|
||||
.toBytesSink(bytesList)
|
||||
|
||||
if (suffix) {
|
||||
bytesList.put(suffix)
|
||||
}
|
||||
|
||||
return bytesList.toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an object for signing
|
||||
*
|
||||
* @param transaction Transaction to serialize
|
||||
* @param prefix Prefix bytes to put before the serialized object
|
||||
* @param opts.definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns A Uint8Array with the serialized object
|
||||
*/
|
||||
function signingData(
|
||||
transaction: JsonObject,
|
||||
prefix: Uint8Array = HashPrefix.transactionSig,
|
||||
opts: { definitions?: XrplDefinitionsBase } = {},
|
||||
): Uint8Array {
|
||||
return serializeObject(transaction, {
|
||||
prefix,
|
||||
signingFieldsOnly: true,
|
||||
definitions: opts.definitions,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing fields required for a Claim
|
||||
*/
|
||||
interface ClaimObject extends JsonObject {
|
||||
channel: string
|
||||
amount: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a signingClaim
|
||||
*
|
||||
* @param claim A claim object to serialize
|
||||
* @param opts.definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns the serialized object with appropriate prefix
|
||||
*/
|
||||
function signingClaimData(claim: ClaimObject): Uint8Array {
|
||||
const num = BigInt(String(claim.amount))
|
||||
const prefix = HashPrefix.paymentChannelClaim
|
||||
const channel = coreTypes.Hash256.from(claim.channel).toBytes()
|
||||
const amount = coreTypes.UInt64.from(num).toBytes()
|
||||
|
||||
const bytesList = new BytesList()
|
||||
|
||||
bytesList.put(prefix)
|
||||
bytesList.put(channel)
|
||||
bytesList.put(amount)
|
||||
return bytesList.toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a transaction object for multiSigning
|
||||
*
|
||||
* @param transaction transaction to serialize
|
||||
* @param signingAccount Account to sign the transaction with
|
||||
* @param opts.definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns serialized transaction with appropriate prefix and suffix
|
||||
*/
|
||||
function multiSigningData(
|
||||
transaction: JsonObject,
|
||||
signingAccount: string | AccountID,
|
||||
opts: { definitions: XrplDefinitionsBase } = {
|
||||
definitions: DEFAULT_DEFINITIONS,
|
||||
},
|
||||
): Uint8Array {
|
||||
const prefix = HashPrefix.transactionMultiSig
|
||||
const suffix = coreTypes.AccountID.from(signingAccount).toBytes()
|
||||
return serializeObject(transaction, {
|
||||
prefix,
|
||||
suffix,
|
||||
signingFieldsOnly: true,
|
||||
definitions: opts.definitions,
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
BinaryParser,
|
||||
BinarySerializer,
|
||||
BytesList,
|
||||
ClaimObject,
|
||||
makeParser,
|
||||
serializeObject,
|
||||
readJSON,
|
||||
multiSigningData,
|
||||
signingData,
|
||||
signingClaimData,
|
||||
binaryToJSON,
|
||||
sha512Half,
|
||||
transactionID,
|
||||
}
|
||||
31
packages/xahau-binary-codec/src/coretypes.ts
Normal file
31
packages/xahau-binary-codec/src/coretypes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
DEFAULT_DEFINITIONS,
|
||||
Field,
|
||||
TransactionType,
|
||||
LedgerEntryType,
|
||||
Type,
|
||||
TransactionResult,
|
||||
} from './enums'
|
||||
import * as types from './types'
|
||||
import * as binary from './binary'
|
||||
import { ShaMap } from './shamap'
|
||||
import * as ledgerHashes from './ledger-hashes'
|
||||
import * as hashes from './hashes'
|
||||
import { quality } from './quality'
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
|
||||
export {
|
||||
hashes,
|
||||
binary,
|
||||
ledgerHashes,
|
||||
DEFAULT_DEFINITIONS,
|
||||
Field,
|
||||
TransactionType,
|
||||
LedgerEntryType,
|
||||
Type,
|
||||
TransactionResult,
|
||||
quality,
|
||||
HashPrefix,
|
||||
ShaMap,
|
||||
types,
|
||||
}
|
||||
144
packages/xahau-binary-codec/src/enums/README.md
Normal file
144
packages/xahau-binary-codec/src/enums/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Definitions
|
||||
|
||||
This file is used to serialize/deserialize transactions and ledger objects for the XRPL. It's broken into 5 sections laid out below.
|
||||
|
||||
At the bottom of this README you can find instructions and examples for how to define your own types in a definitions file in order to work on a custom sidechain or develop new amendments.
|
||||
|
||||
## Types
|
||||
|
||||
These are the [types](https://xrpl.org/serialization.html#type-list) associated with a given Serialization Field. Each type has an arbitrary [type_code](https://xrpl.org/serialization.html#type-codes), with lower codes sorting first.
|
||||
|
||||
## Ledger Entry Types
|
||||
|
||||
Each ledger's state tree contain [ledger objects](https://xrpl.org/ledger-object-types.html), which represent all settings, balances, and relationships in the shared ledger.
|
||||
|
||||
## Fields
|
||||
|
||||
These are Serialization Fields (`sf`) [defined in rippled's SField.cpp](https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/impl/SField.cpp). Fields with undefined values are omitted before encoding.
|
||||
|
||||
### Key
|
||||
|
||||
The key is the string defined in the rippled source code, such as "LedgerEntry", "Transaction", etc.
|
||||
|
||||
### nth
|
||||
|
||||
`nth` is the sort code, meaning "nth of type." It is is combined with the type code in order to construct the Field ID of this field. The Field ID is only used for sorting the fields. Since there are multiple fields with the same data type, the `nth` is used to deterministically order each field among other fields of the same data type.
|
||||
|
||||
Each field has a Field ID, which is used to sort fields that have the same type as one another with lower codes sorting first.
|
||||
|
||||
- [Field definitions](https://github.com/ripple/rippled/blob/72e6005f562a8f0818bc94803d222ac9345e1e40/src/ripple/protocol/impl/SField.cpp#L72-L266)
|
||||
- [Constructing the `SField` field codes](https://github.com/ripple/rippled/blob/eaff9a0e6aec0ad077f118501791c7684debcfd5/src/ripple/protocol/SField.h#L95-L98)
|
||||
|
||||
For example, the `Account` field has sort code (nth) `1`, so it comes before the `Destination` field which has sort code `3`.
|
||||
|
||||
Sort code numbers are reused for fields of different types, but different fields of the same type never have the same sort code. When you combine the type code with the sort code, you get the field's unique _Field ID_.
|
||||
|
||||
The unique [Field ID](https://xrpl.org/serialization.html#field-ids) is prefixed before the field in the final serialized blob. The size of the Field ID is one to three bytes depending on the type code and the field codes it combines.
|
||||
|
||||
### isVLEncoded
|
||||
|
||||
If true, the field is Variable Length encoded and [length-prefixed](https://xrpl.org/serialization.html#length-prefixing). The variable-length encoded fields are `STI_VL`/`Blob`, `STI_ACCOUNT`/`AccountID`, and `STI_VECTOR256`/`Vector256`.
|
||||
|
||||
### isSerialized
|
||||
|
||||
Fields are serialized if they are not [one of these](https://github.com/ripple/rippled/blob/eaff9a0e6aec0ad077f118501791c7684debcfd5/src/ripple/protocol/impl/SField.cpp#L71-L78) or if they are not an SField.
|
||||
|
||||
- https://github.com/ripple/ripple-binary-codec/blob/14e76e68ead7e4bcd83c942dbdc9064d5a66869b/src/enums/definitions.json#L832
|
||||
- https://github.com/ripple/rippled/search?utf8=%E2%9C%93&q=taker_gets_funded&type=
|
||||
|
||||
### isSigningField
|
||||
|
||||
True unless the field is [specified with `SField::notSigning`](https://github.com/ripple/rippled/blob/eaff9a0e6aec0ad077f118501791c7684debcfd5/src/ripple/protocol/impl/SField.cpp#L198).
|
||||
|
||||
## Transaction Results
|
||||
|
||||
See:
|
||||
|
||||
- https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/TER.h
|
||||
- https://xrpl.org/transaction-results.html
|
||||
|
||||
To generate a new definitions file from rippled source code, use this tool: https://github.com/RichardAH/xrpl-codec-gen
|
||||
|
||||
## Transaction Types
|
||||
|
||||
See https://github.com/ripple/rippled/blob/develop/src/ripple/protocol/TxFormats.h
|
||||
|
||||
# Defining Your Own Definitions
|
||||
|
||||
If you're building your own sidechain or writing an amendment for the XRPL, you may need to create new XRPL definitions.
|
||||
|
||||
To do that there are a couple things you need to do:
|
||||
|
||||
1. Generate your own `definitions.json` file from rippled source code using [this tool](https://github.com/RichardAH/xrpl-codec-gen) (The default `definitions.json` for mainnet can be found [here](https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json))
|
||||
2. Create new SerializedType classes for any new Types (So that encode/decode behavior is defined). The SerializedType classes correspond to "ST..." classes in Rippled. Note: This is very rarely required.
|
||||
|
||||
- For examples of how to implement that you can look at objects in the [`types` folder](../types/), such as `Amount`, `UInt8`, or `STArray`.
|
||||
|
||||
3. Import your `definitions.json` file to construct your own `XrplDefinitions` object.
|
||||
4. Pass the `XrplDefinitions` object whenever you `encode` or `decode` a transaction.
|
||||
5. If you added any new transaction types, you should create an `interface` for the transaction that extends `BaseTransaction` from the `xrpl` repo to use it with the functions on `Client` (See the below example of adding a new transaction type)
|
||||
|
||||
## Example of adding a new Transaction type
|
||||
|
||||
```
|
||||
// newDefinitionsJson is where you can import your custom defined definitions.json file
|
||||
const newDefinitionsJson = require('./new-transaction-type-definitions.json')
|
||||
const { XrplDefinitions, Client } = require('xrpl')
|
||||
|
||||
const newDefs = new XrplDefinitions(newDefinitionsJson)
|
||||
|
||||
// Change to point at the server you care about
|
||||
const serverAddress = 'wss://s.devnet.rippletest.net:51233'
|
||||
const client = new Client(serverAddress)
|
||||
const wallet1 = await client.fundWallet()
|
||||
|
||||
// Extending BaseTransaction allows typescript to recognize this as a transaction type
|
||||
interface NewTx extends BaseTransaction {
|
||||
Amount: Amount
|
||||
}
|
||||
|
||||
const tx: NewTx = {
|
||||
// The TransactionType here needs to match what you added in your newDefinitionsJson file
|
||||
TransactionType: 'NewTx',
|
||||
Account: wallet1.address,
|
||||
Amount: '100',
|
||||
}
|
||||
|
||||
// By passing in your newDefs, your new transaction should be serializable.
|
||||
// Rippled will still throw an error though if it's not a supported transaction type.
|
||||
const result = await client.submitAndWait(tx, {
|
||||
wallet: wallet1,
|
||||
definitions: newDefs,
|
||||
})
|
||||
```
|
||||
|
||||
## Example of adding a new serializable Type
|
||||
|
||||
```
|
||||
const { XrplDefinitions } = require('../dist/coretypes')
|
||||
|
||||
// newDefinitionsJson is where you can import your custom defined definitions.json file
|
||||
const newDefinitionsJson = require('./fixtures/new-definitions.json')
|
||||
|
||||
|
||||
// For any new Types you create, you'll need to make a class with the same name which extends a SerializedType object
|
||||
// In order to define how to serialize/deserialize that field. Here we simply make our NewType act like a UInt32.
|
||||
|
||||
const { UInt32 } = require('../dist/types/uint-32')
|
||||
class NewType extends UInt32 {
|
||||
// Should be the same as UInt32
|
||||
}
|
||||
|
||||
const extendedCoreTypes = { NewType }
|
||||
|
||||
const newDefs = new XrplDefinitions(newDefinitionsJson, extendedCoreTypes)
|
||||
|
||||
// From this point on, we should be able to serialize / deserialize Transactions with fields that have 'NewType' as their Type.
|
||||
|
||||
const encoded = encode(my_tx, newDefs)
|
||||
const decoded = decode(encoded, newDefs)
|
||||
```
|
||||
|
||||
## Other examples
|
||||
|
||||
You can find other examples of how to modify `definitions.json` in `definition.test.js` which contains tests for this feature, and uses various example modified `definition` files. You can find the tests and the corresponding example `definition` files in [this folder of test cases](https://github.com/XRPLF/xrpl.js/tree/main/packages/ripple-binary-codec/test)
|
||||
74
packages/xahau-binary-codec/src/enums/bytes.ts
Normal file
74
packages/xahau-binary-codec/src/enums/bytes.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { BytesList, BinaryParser } from '../binary'
|
||||
|
||||
/*
|
||||
* @brief: Bytes, name, and ordinal representing one type, ledger_type, transaction type, or result
|
||||
*/
|
||||
export class Bytes {
|
||||
readonly bytes: Uint8Array
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly ordinal: number,
|
||||
readonly ordinalWidth: number,
|
||||
) {
|
||||
this.bytes = new Uint8Array(ordinalWidth)
|
||||
for (let i = 0; i < ordinalWidth; i++) {
|
||||
this.bytes[ordinalWidth - i - 1] = (ordinal >>> (i * 8)) & 0xff
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return this.name
|
||||
}
|
||||
|
||||
toBytesSink(sink: BytesList): void {
|
||||
sink.put(this.bytes)
|
||||
}
|
||||
|
||||
toBytes(): Uint8Array {
|
||||
return this.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief: Collection of Bytes objects, mapping bidirectionally
|
||||
*/
|
||||
export class BytesLookup {
|
||||
constructor(types: Record<string, number>, readonly ordinalWidth: number) {
|
||||
Object.entries(types).forEach(([k, v]) => {
|
||||
this.add(k, v)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new name value pair to the BytesLookup.
|
||||
*
|
||||
* @param name - A human readable name for the field.
|
||||
* @param value - The numeric value for the field.
|
||||
* @throws if the name or value already exist in the lookup because it's unclear how to decode.
|
||||
*/
|
||||
add(name: string, value: number): void {
|
||||
if (this[name]) {
|
||||
throw new SyntaxError(
|
||||
`Attempted to add a value with a duplicate name "${name}". This is not allowed because it is unclear how to decode.`,
|
||||
)
|
||||
}
|
||||
if (this[value.toString()]) {
|
||||
throw new SyntaxError(
|
||||
`Attempted to add a duplicate value under a different name (Given name: "${name}" and previous name: "${
|
||||
this[value.toString()]
|
||||
}. This is not allowed because it is unclear how to decode.\nGiven value: ${value.toString()}`,
|
||||
)
|
||||
}
|
||||
this[name] = new Bytes(name, value, this.ordinalWidth)
|
||||
this[value.toString()] = this[name]
|
||||
}
|
||||
|
||||
from(value: Bytes | string): Bytes {
|
||||
return value instanceof Bytes ? value : (this[value] as Bytes)
|
||||
}
|
||||
|
||||
fromParser(parser: BinaryParser): Bytes {
|
||||
return this.from(parser.readUIntN(this.ordinalWidth).toString())
|
||||
}
|
||||
}
|
||||
4
packages/xahau-binary-codec/src/enums/constants.ts
Normal file
4
packages/xahau-binary-codec/src/enums/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const TYPE_WIDTH = 2
|
||||
export const LEDGER_ENTRY_WIDTH = 2
|
||||
export const TRANSACTION_TYPE_WIDTH = 2
|
||||
export const TRANSACTION_RESULT_WIDTH = 1
|
||||
2981
packages/xahau-binary-codec/src/enums/definitions.json
Normal file
2981
packages/xahau-binary-codec/src/enums/definitions.json
Normal file
File diff suppressed because it is too large
Load Diff
84
packages/xahau-binary-codec/src/enums/field.ts
Normal file
84
packages/xahau-binary-codec/src/enums/field.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Bytes } from './bytes'
|
||||
import { SerializedType } from '../types/serialized-type'
|
||||
import { TYPE_WIDTH } from './constants'
|
||||
|
||||
/**
|
||||
* Encoding information for a xahaud field, often used in transactions.
|
||||
* See the enums [README.md](https://github.com/XRPLF/xrpl.js/tree/main/packages/xahau-binary-codec/src/enums) for more details on what each means.
|
||||
*/
|
||||
export interface FieldInfo {
|
||||
nth: number
|
||||
isVLEncoded: boolean
|
||||
isSerialized: boolean
|
||||
isSigningField: boolean
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface FieldInstance {
|
||||
readonly nth: number
|
||||
readonly isVariableLengthEncoded: boolean
|
||||
readonly isSerialized: boolean
|
||||
readonly isSigningField: boolean
|
||||
readonly type: Bytes
|
||||
readonly ordinal: number
|
||||
readonly name: string
|
||||
readonly header: Uint8Array
|
||||
readonly associatedType: typeof SerializedType
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief: Serialize a field based on type_code and Field.nth
|
||||
*/
|
||||
function fieldHeader(type: number, nth: number): Uint8Array {
|
||||
const header: Array<number> = []
|
||||
if (type < 16) {
|
||||
if (nth < 16) {
|
||||
header.push((type << 4) | nth)
|
||||
} else {
|
||||
header.push(type << 4, nth)
|
||||
}
|
||||
} else if (nth < 16) {
|
||||
header.push(nth, type)
|
||||
} else {
|
||||
header.push(0, type, nth)
|
||||
}
|
||||
return Uint8Array.from(header)
|
||||
}
|
||||
|
||||
function buildField(
|
||||
[name, info]: [string, FieldInfo],
|
||||
typeOrdinal: number,
|
||||
): FieldInstance {
|
||||
const field = fieldHeader(typeOrdinal, info.nth)
|
||||
return {
|
||||
name: name,
|
||||
nth: info.nth,
|
||||
isVariableLengthEncoded: info.isVLEncoded,
|
||||
isSerialized: info.isSerialized,
|
||||
isSigningField: info.isSigningField,
|
||||
ordinal: (typeOrdinal << 16) | info.nth,
|
||||
type: new Bytes(info.type, typeOrdinal, TYPE_WIDTH),
|
||||
header: field,
|
||||
associatedType: SerializedType, // For later assignment in ./types/index.js or Definitions.updateAll(...)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief: The collection of all fields as defined in definitions.json
|
||||
*/
|
||||
export class FieldLookup {
|
||||
constructor(
|
||||
fields: Array<[string, FieldInfo]>,
|
||||
types: Record<string, number>,
|
||||
) {
|
||||
fields.forEach(([name, field_info]) => {
|
||||
const typeOrdinal = types[field_info.type]
|
||||
this[name] = buildField([name, field_info], typeOrdinal)
|
||||
this[this[name].ordinal.toString()] = this[name]
|
||||
})
|
||||
}
|
||||
|
||||
fromString(value: string): FieldInstance {
|
||||
return this[value] as FieldInstance
|
||||
}
|
||||
}
|
||||
34
packages/xahau-binary-codec/src/enums/index.ts
Normal file
34
packages/xahau-binary-codec/src/enums/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import enums from './definitions.json'
|
||||
import {
|
||||
XrplDefinitionsBase,
|
||||
FieldInstance,
|
||||
Bytes,
|
||||
} from './xahau-definitions-base'
|
||||
/**
|
||||
* By default, coreTypes from the `types` folder is where known type definitions are initialized to avoid import cycles.
|
||||
*/
|
||||
const DEFAULT_DEFINITIONS = new XrplDefinitionsBase(enums, {})
|
||||
|
||||
const Type = DEFAULT_DEFINITIONS.type
|
||||
const LedgerEntryType = DEFAULT_DEFINITIONS.ledgerEntryType
|
||||
const TransactionType = DEFAULT_DEFINITIONS.transactionType
|
||||
const TransactionResult = DEFAULT_DEFINITIONS.transactionResult
|
||||
const Field = DEFAULT_DEFINITIONS.field
|
||||
|
||||
/*
|
||||
* @brief: All valid transaction types
|
||||
*/
|
||||
const TRANSACTION_TYPES = DEFAULT_DEFINITIONS.transactionNames
|
||||
|
||||
export {
|
||||
Bytes,
|
||||
XrplDefinitionsBase,
|
||||
DEFAULT_DEFINITIONS,
|
||||
Field,
|
||||
FieldInstance,
|
||||
Type,
|
||||
LedgerEntryType,
|
||||
TransactionResult,
|
||||
TransactionType,
|
||||
TRANSACTION_TYPES,
|
||||
}
|
||||
134
packages/xahau-binary-codec/src/enums/utils-renumber.ts
Normal file
134
packages/xahau-binary-codec/src/enums/utils-renumber.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Quick script to re-number values
|
||||
*/
|
||||
|
||||
const input = {
|
||||
temBAD_SEND_XAH_PATHS: -283,
|
||||
temBAD_SEQUENCE: -282,
|
||||
temBAD_SIGNATURE: -281,
|
||||
temBAD_SRC_ACCOUNT: -280,
|
||||
temBAD_TRANSFER_RATE: -279,
|
||||
temDST_IS_SRC: -278,
|
||||
temDST_NEEDED: -277,
|
||||
temINVALID: -276,
|
||||
temINVALID_FLAG: -275,
|
||||
temREDUNDANT: -274,
|
||||
temRIPPLE_EMPTY: -273,
|
||||
temDISABLED: -272,
|
||||
temBAD_SIGNER: -271,
|
||||
temBAD_QUORUM: -270,
|
||||
temBAD_WEIGHT: -269,
|
||||
temBAD_TICK_SIZE: -268,
|
||||
temINVALID_ACCOUNT_ID: -267,
|
||||
temCANNOT_PREAUTH_SELF: -266,
|
||||
|
||||
temUNCERTAIN: -265,
|
||||
temUNKNOWN: -264,
|
||||
|
||||
tefFAILURE: -199,
|
||||
tefALREADY: -198,
|
||||
tefBAD_ADD_AUTH: -197,
|
||||
tefBAD_AUTH: -196,
|
||||
tefBAD_LEDGER: -195,
|
||||
tefCREATED: -194,
|
||||
tefEXCEPTION: -193,
|
||||
tefINTERNAL: -192,
|
||||
tefNO_AUTH_REQUIRED: -191,
|
||||
tefPAST_SEQ: -190,
|
||||
tefWRONG_PRIOR: -189,
|
||||
tefMASTER_DISABLED: -188,
|
||||
tefMAX_LEDGER: -187,
|
||||
tefBAD_SIGNATURE: -186,
|
||||
tefBAD_QUORUM: -185,
|
||||
tefNOT_MULTI_SIGNING: -184,
|
||||
tefBAD_AUTH_MASTER: -183,
|
||||
tefINVARIANT_FAILED: -182,
|
||||
tefTOO_BIG: -181,
|
||||
|
||||
terRETRY: -99,
|
||||
terFUNDS_SPENT: -98,
|
||||
terINSUF_FEE_B: -97,
|
||||
terNO_ACCOUNT: -96,
|
||||
terNO_AUTH: -95,
|
||||
terNO_LINE: -94,
|
||||
terOWNERS: -93,
|
||||
terPRE_SEQ: -92,
|
||||
terLAST: -91,
|
||||
terNO_RIPPLE: -90,
|
||||
terQUEUED: -89,
|
||||
|
||||
tesSUCCESS: 0,
|
||||
|
||||
tecCLAIM: 100,
|
||||
tecPATH_PARTIAL: 101,
|
||||
tecUNFUNDED_ADD: 102,
|
||||
tecUNFUNDED_OFFER: 103,
|
||||
tecUNFUNDED_PAYMENT: 104,
|
||||
tecFAILED_PROCESSING: 105,
|
||||
tecDIR_FULL: 121,
|
||||
tecINSUF_RESERVE_LINE: 122,
|
||||
tecINSUF_RESERVE_OFFER: 123,
|
||||
tecNO_DST: 124,
|
||||
tecNO_DST_INSUF_XAH: 125,
|
||||
tecNO_LINE_INSUF_RESERVE: 126,
|
||||
tecNO_LINE_REDUNDANT: 127,
|
||||
tecPATH_DRY: 128,
|
||||
tecUNFUNDED: 129,
|
||||
tecNO_ALTERNATIVE_KEY: 130,
|
||||
tecNO_REGULAR_KEY: 131,
|
||||
tecOWNERS: 132,
|
||||
tecNO_ISSUER: 133,
|
||||
tecNO_AUTH: 134,
|
||||
tecNO_LINE: 135,
|
||||
tecINSUFF_FEE: 136,
|
||||
tecFROZEN: 137,
|
||||
tecNO_TARGET: 138,
|
||||
tecNO_PERMISSION: 139,
|
||||
tecNO_ENTRY: 140,
|
||||
tecINSUFFICIENT_RESERVE: 141,
|
||||
tecNEED_MASTER_KEY: 142,
|
||||
tecDST_TAG_NEEDED: 143,
|
||||
tecINTERNAL: 144,
|
||||
tecOVERSIZE: 145,
|
||||
tecCRYPTOCONDITION_ERROR: 146,
|
||||
tecINVARIANT_FAILED: 147,
|
||||
tecEXPIRED: 148,
|
||||
tecDUPLICATE: 149,
|
||||
tecKILLED: 150,
|
||||
tecHAS_OBLIGATIONS: 151,
|
||||
tecTOO_SOON: 152,
|
||||
}
|
||||
|
||||
let startingFromTemBADSENDXAHPATHS = -284
|
||||
|
||||
let startingFromTefFAILURE = -199
|
||||
|
||||
let startingFromTerRETRY = -99
|
||||
|
||||
const tesSUCCESS = 0
|
||||
|
||||
let startingFromTecCLAIM = 100
|
||||
|
||||
const startingFromTecDIRFULL = 121
|
||||
|
||||
let previousKey = 'tem'
|
||||
Object.keys(input).forEach((key) => {
|
||||
if (key.substring(0, 3) !== previousKey.substring(0, 3)) {
|
||||
console.log()
|
||||
previousKey = key
|
||||
}
|
||||
if (key.substring(0, 3) === 'tem') {
|
||||
console.log(` "${key}": ${startingFromTemBADSENDXAHPATHS++},`)
|
||||
} else if (key.substring(0, 3) === 'tef') {
|
||||
console.log(` "${key}": ${startingFromTefFAILURE++},`)
|
||||
} else if (key.substring(0, 3) === 'ter') {
|
||||
console.log(` "${key}": ${startingFromTerRETRY++},`)
|
||||
} else if (key.substring(0, 3) === 'tes') {
|
||||
console.log(` "${key}": ${tesSUCCESS},`)
|
||||
} else if (key.substring(0, 3) === 'tec') {
|
||||
if (key === 'tecDIR_FULL') {
|
||||
startingFromTecCLAIM = startingFromTecDIRFULL
|
||||
}
|
||||
console.log(` "${key}": ${startingFromTecCLAIM++},`)
|
||||
}
|
||||
})
|
||||
111
packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts
Normal file
111
packages/xahau-binary-codec/src/enums/xahau-definitions-base.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { SerializedType } from '../types/serialized-type'
|
||||
import { Bytes, BytesLookup } from './bytes'
|
||||
import { FieldInfo, FieldLookup, FieldInstance } from './field'
|
||||
import {
|
||||
TYPE_WIDTH,
|
||||
LEDGER_ENTRY_WIDTH,
|
||||
TRANSACTION_TYPE_WIDTH,
|
||||
TRANSACTION_RESULT_WIDTH,
|
||||
} from './constants'
|
||||
|
||||
interface DefinitionsData {
|
||||
TYPES: Record<string, number>
|
||||
LEDGER_ENTRY_TYPES: Record<string, number>
|
||||
FIELDS: (string | FieldInfo)[][]
|
||||
TRANSACTION_RESULTS: Record<string, number>
|
||||
TRANSACTION_TYPES: Record<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the various types and fields for xahaud to be used to encode/decode information later on.
|
||||
* XrplDefinitions should be instantiated instead of this class.
|
||||
*/
|
||||
class XrplDefinitionsBase {
|
||||
// A collection of fields that can be included in transactions
|
||||
field: FieldLookup
|
||||
// A collection of ids corresponding to types of ledger objects
|
||||
ledgerEntryType: BytesLookup
|
||||
// A collection of type flags used to determine how to serialize a field's data
|
||||
type: BytesLookup
|
||||
// Errors and result codes for transactions
|
||||
transactionResult: BytesLookup
|
||||
// Defined transactions that can be submitted to the ledger
|
||||
transactionType: BytesLookup
|
||||
// Valid transaction names
|
||||
transactionNames: string[]
|
||||
// Maps serializable types to their TypeScript class implementation
|
||||
dataTypes: Record<string, typeof SerializedType>
|
||||
|
||||
/**
|
||||
* Present xahaud types in a typed and updatable format.
|
||||
* For an example of the input format see `definitions.json`
|
||||
* To generate a new definitions file from xahaud source code, use this tool: https://github.com/RichardAH/xrpl-codec-gen
|
||||
*
|
||||
* See the definitions.test.js file for examples of how to create your own updated definitions.json.
|
||||
*
|
||||
* @param enums - A json encoding of the core types, transaction types, transaction results, transaction names, and fields.
|
||||
* @param types - A list of type objects with the same name as the fields defined.
|
||||
* You can use the coreTypes object if you are not adding new types.
|
||||
*/
|
||||
constructor(
|
||||
enums: DefinitionsData,
|
||||
types: Record<string, typeof SerializedType>,
|
||||
) {
|
||||
this.type = new BytesLookup(enums.TYPES, TYPE_WIDTH)
|
||||
this.ledgerEntryType = new BytesLookup(
|
||||
enums.LEDGER_ENTRY_TYPES,
|
||||
LEDGER_ENTRY_WIDTH,
|
||||
)
|
||||
this.transactionType = new BytesLookup(
|
||||
enums.TRANSACTION_TYPES,
|
||||
TRANSACTION_TYPE_WIDTH,
|
||||
)
|
||||
this.transactionResult = new BytesLookup(
|
||||
enums.TRANSACTION_RESULTS,
|
||||
TRANSACTION_RESULT_WIDTH,
|
||||
)
|
||||
this.field = new FieldLookup(
|
||||
enums.FIELDS as Array<[string, FieldInfo]>,
|
||||
enums.TYPES,
|
||||
)
|
||||
this.transactionNames = Object.entries(enums.TRANSACTION_TYPES)
|
||||
.filter(([_key, value]) => value >= 0)
|
||||
.map(([key, _value]) => key)
|
||||
|
||||
this.dataTypes = {} // Filled in via associateTypes
|
||||
this.associateTypes(types)
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates each Field to a corresponding class that TypeScript can recognize.
|
||||
*
|
||||
* @param types a list of type objects with the same name as the fields defined.
|
||||
* Defaults to xrpl.js's core type definitions.
|
||||
*/
|
||||
public associateTypes(types: Record<string, typeof SerializedType>): void {
|
||||
// Overwrite any existing type definitions with the given types
|
||||
this.dataTypes = Object.assign({}, this.dataTypes, types)
|
||||
|
||||
Object.values(this.field).forEach((field) => {
|
||||
field.associatedType = this.dataTypes[field.type.name]
|
||||
})
|
||||
|
||||
this.field['TransactionType'].associatedType = this.transactionType
|
||||
this.field['TransactionResult'].associatedType = this.transactionResult
|
||||
this.field['LedgerEntryType'].associatedType = this.ledgerEntryType
|
||||
}
|
||||
|
||||
public getAssociatedTypes(): Record<string, typeof SerializedType> {
|
||||
return this.dataTypes
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DefinitionsData,
|
||||
XrplDefinitionsBase,
|
||||
FieldLookup,
|
||||
FieldInfo,
|
||||
FieldInstance,
|
||||
Bytes,
|
||||
BytesLookup,
|
||||
}
|
||||
32
packages/xahau-binary-codec/src/enums/xahau-definitions.ts
Normal file
32
packages/xahau-binary-codec/src/enums/xahau-definitions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type DefinitionsData,
|
||||
XrplDefinitionsBase,
|
||||
} from './xahau-definitions-base'
|
||||
import { coreTypes } from '../types'
|
||||
import { SerializedType } from '../types/serialized-type'
|
||||
|
||||
/**
|
||||
* Stores the various types and fields for xahaud to be used to encode/decode information later on.
|
||||
* Should be used instead of XrplDefinitionsBase since this defines default `types` for serializing/deserializing
|
||||
* ledger data.
|
||||
*/
|
||||
export class XrplDefinitions extends XrplDefinitionsBase {
|
||||
/**
|
||||
* Present xahaud types in a typed and updatable format.
|
||||
* For an example of the input format see `definitions.json`
|
||||
* To generate a new definitions file from xahaud source code, use this tool: https://github.com/RichardAH/xrpl-codec-gen
|
||||
*
|
||||
* See the definitions.test.js file for examples of how to create your own updated definitions.json.
|
||||
*
|
||||
* @param enums - A json encoding of the core types, transaction types, transaction results, transaction names, and fields.
|
||||
* @param additionalTypes - A list of SerializedType objects with the same name as the fields defined.
|
||||
* These types will be included in addition to the coreTypes used on mainnet.
|
||||
*/
|
||||
constructor(
|
||||
enums: DefinitionsData,
|
||||
additionalTypes?: Record<string, typeof SerializedType>,
|
||||
) {
|
||||
const types = Object.assign({}, coreTypes, additionalTypes)
|
||||
super(enums, types)
|
||||
}
|
||||
}
|
||||
40
packages/xahau-binary-codec/src/hash-prefixes.ts
Normal file
40
packages/xahau-binary-codec/src/hash-prefixes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { writeUInt32BE } from './utils'
|
||||
|
||||
/**
|
||||
* Write a 32 bit integer to a Uint8Array
|
||||
*
|
||||
* @param uint32 32 bit integer to write to Uint8Array
|
||||
* @returns a Uint8Array with the bytes representation of uint32
|
||||
*/
|
||||
function bytes(uint32: number): Uint8Array {
|
||||
const result = new Uint8Array(4)
|
||||
writeUInt32BE(result, uint32, 0)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps HashPrefix names to their byte representation
|
||||
*/
|
||||
const HashPrefix: Record<string, Uint8Array> = {
|
||||
transactionID: bytes(0x54584e00),
|
||||
// transaction plus metadata
|
||||
transaction: bytes(0x534e4400),
|
||||
// account state
|
||||
accountStateEntry: bytes(0x4d4c4e00),
|
||||
// inner node in tree
|
||||
innerNode: bytes(0x4d494e00),
|
||||
// ledger master data for signing
|
||||
ledgerHeader: bytes(0x4c575200),
|
||||
// inner transaction to sign
|
||||
transactionSig: bytes(0x53545800),
|
||||
// inner transaction to sign
|
||||
transactionMultiSig: bytes(0x534d5400),
|
||||
// validation for signing
|
||||
validation: bytes(0x56414c00),
|
||||
// proposal for signing
|
||||
proposal: bytes(0x50525000),
|
||||
// payment channel claim
|
||||
paymentChannelClaim: bytes(0x434c4d00),
|
||||
}
|
||||
|
||||
export { HashPrefix }
|
||||
75
packages/xahau-binary-codec/src/hashes.ts
Normal file
75
packages/xahau-binary-codec/src/hashes.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
import { Hash256 } from './types'
|
||||
import { BytesList } from './serdes/binary-serializer'
|
||||
import { sha512 } from '@xrplf/isomorphic/sha512'
|
||||
|
||||
/**
|
||||
* Class for hashing with SHA512
|
||||
* @extends BytesList So SerializedTypes can write bytes to a Sha512Half
|
||||
*/
|
||||
class Sha512Half extends BytesList {
|
||||
private hash = sha512.create()
|
||||
|
||||
/**
|
||||
* Construct a new Sha512Hash and write bytes this.hash
|
||||
*
|
||||
* @param bytes bytes to write to this.hash
|
||||
* @returns the new Sha512Hash object
|
||||
*/
|
||||
static put(bytes: Uint8Array): Sha512Half {
|
||||
return new Sha512Half().put(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write bytes to an existing Sha512Hash
|
||||
*
|
||||
* @param bytes bytes to write to object
|
||||
* @returns the Sha512 object
|
||||
*/
|
||||
put(bytes: Uint8Array): Sha512Half {
|
||||
this.hash.update(bytes)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA512 hash and slice in half
|
||||
*
|
||||
* @returns half of a SHA512 hash
|
||||
*/
|
||||
finish256(): Uint8Array {
|
||||
return Uint8Array.from(this.hash.digest().slice(0, 32))
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Hash256 from the Sha512Half object
|
||||
*
|
||||
* @returns a Hash256 object
|
||||
*/
|
||||
finish(): Hash256 {
|
||||
return new Hash256(this.finish256())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* compute SHA512 hash of a list of bytes
|
||||
*
|
||||
* @param args zero or more arguments to hash
|
||||
* @returns the sha512half hash of the arguments.
|
||||
*/
|
||||
function sha512Half(...args: Uint8Array[]): Uint8Array {
|
||||
const hash = new Sha512Half()
|
||||
args.forEach((a) => hash.put(a))
|
||||
return hash.finish256()
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a transactionID from a Serialized Transaction
|
||||
*
|
||||
* @param serialized bytes to hash
|
||||
* @returns a Hash256 object
|
||||
*/
|
||||
function transactionID(serialized: Uint8Array): Hash256 {
|
||||
return new Hash256(sha512Half(HashPrefix.transactionID, serialized))
|
||||
}
|
||||
|
||||
export { Sha512Half, sha512Half, transactionID }
|
||||
153
packages/xahau-binary-codec/src/index.ts
Normal file
153
packages/xahau-binary-codec/src/index.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { quality, binary, HashPrefix } from './coretypes'
|
||||
import { decodeLedgerData } from './ledger-hashes'
|
||||
import { ClaimObject } from './binary'
|
||||
import { JsonObject } from './types/serialized-type'
|
||||
import {
|
||||
XrplDefinitionsBase,
|
||||
TRANSACTION_TYPES,
|
||||
DEFAULT_DEFINITIONS,
|
||||
} from './enums'
|
||||
import { XrplDefinitions } from './enums/xahau-definitions'
|
||||
import { coreTypes } from './types'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
const {
|
||||
signingData,
|
||||
signingClaimData,
|
||||
multiSigningData,
|
||||
binaryToJSON,
|
||||
serializeObject,
|
||||
} = binary
|
||||
|
||||
/**
|
||||
* Decode a transaction
|
||||
*
|
||||
* @param binary hex-string of the encoded transaction
|
||||
* @param definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns the JSON representation of the transaction
|
||||
*/
|
||||
function decode(binary: string, definitions?: XrplDefinitionsBase): JsonObject {
|
||||
if (typeof binary !== 'string') {
|
||||
throw new Error('binary must be a hex string')
|
||||
}
|
||||
return binaryToJSON(binary, definitions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a transaction
|
||||
*
|
||||
* @param json The JSON representation of a transaction
|
||||
* @param definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
*
|
||||
* @returns A hex-string of the encoded transaction
|
||||
*/
|
||||
function encode(json: object, definitions?: XrplDefinitionsBase): string {
|
||||
if (typeof json !== 'object') {
|
||||
throw new Error()
|
||||
}
|
||||
return bytesToHex(serializeObject(json as JsonObject, { definitions }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a transaction and prepare for signing
|
||||
*
|
||||
* @param json JSON object representing the transaction
|
||||
* @param signer string representing the account to sign the transaction with
|
||||
* @param definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns a hex string of the encoded transaction
|
||||
*/
|
||||
function encodeForSigning(
|
||||
json: object,
|
||||
definitions?: XrplDefinitionsBase,
|
||||
): string {
|
||||
if (typeof json !== 'object') {
|
||||
throw new Error()
|
||||
}
|
||||
return bytesToHex(
|
||||
signingData(json as JsonObject, HashPrefix.transactionSig, {
|
||||
definitions,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a transaction and prepare for signing with a claim
|
||||
*
|
||||
* @param json JSON object representing the transaction
|
||||
* @param signer string representing the account to sign the transaction with
|
||||
* @param definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns a hex string of the encoded transaction
|
||||
*/
|
||||
function encodeForSigningClaim(json: object): string {
|
||||
if (typeof json !== 'object') {
|
||||
throw new Error()
|
||||
}
|
||||
return bytesToHex(signingClaimData(json as ClaimObject))
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a transaction and prepare for multi-signing
|
||||
*
|
||||
* @param json JSON object representing the transaction
|
||||
* @param signer string representing the account to sign the transaction with
|
||||
* @param definitions Custom xahaud types to use instead of the default. Used for sidechains and amendments.
|
||||
* @returns a hex string of the encoded transaction
|
||||
*/
|
||||
function encodeForMultisigning(
|
||||
json: object,
|
||||
signer: string,
|
||||
definitions?: XrplDefinitionsBase,
|
||||
): string {
|
||||
if (typeof json !== 'object') {
|
||||
throw new Error()
|
||||
}
|
||||
if (json['SigningPubKey'] !== '') {
|
||||
throw new Error()
|
||||
}
|
||||
const definitionsOpt = definitions ? { definitions } : undefined
|
||||
return bytesToHex(
|
||||
multiSigningData(json as JsonObject, signer, definitionsOpt),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a quality value
|
||||
*
|
||||
* @param value string representation of a number
|
||||
* @returns a hex-string representing the quality
|
||||
*/
|
||||
function encodeQuality(value: string): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error()
|
||||
}
|
||||
return bytesToHex(quality.encode(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a quality value
|
||||
*
|
||||
* @param value hex-string of a quality
|
||||
* @returns a string representing the quality
|
||||
*/
|
||||
function decodeQuality(value: string): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error()
|
||||
}
|
||||
return quality.decode(value).toString()
|
||||
}
|
||||
|
||||
export {
|
||||
decode,
|
||||
encode,
|
||||
encodeForSigning,
|
||||
encodeForSigningClaim,
|
||||
encodeForMultisigning,
|
||||
encodeQuality,
|
||||
decodeQuality,
|
||||
decodeLedgerData,
|
||||
TRANSACTION_TYPES,
|
||||
XrplDefinitions,
|
||||
XrplDefinitionsBase,
|
||||
DEFAULT_DEFINITIONS,
|
||||
coreTypes,
|
||||
}
|
||||
191
packages/xahau-binary-codec/src/ledger-hashes.ts
Normal file
191
packages/xahau-binary-codec/src/ledger-hashes.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { ShaMap, ShaMapNode, ShaMapLeaf } from './shamap'
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
import { Sha512Half } from './hashes'
|
||||
import { BinarySerializer, serializeObject } from './binary'
|
||||
import { Hash256 } from './types/hash-256'
|
||||
import { STObject } from './types/st-object'
|
||||
import { UInt64 } from './types/uint-64'
|
||||
import { UInt32 } from './types/uint-32'
|
||||
import { UInt8 } from './types/uint-8'
|
||||
import { BinaryParser } from './serdes/binary-parser'
|
||||
import { JsonObject } from './types/serialized-type'
|
||||
import { XrplDefinitionsBase } from './enums'
|
||||
|
||||
/**
|
||||
* Computes the hash of a list of objects
|
||||
*
|
||||
* @param itemizer Converts an item into a format that can be added to SHAMap
|
||||
* @param itemsJson Array of items to add to a SHAMap
|
||||
* @returns the hash of the SHAMap
|
||||
*/
|
||||
function computeHash(
|
||||
itemizer: (item: JsonObject) => [Hash256?, ShaMapNode?, ShaMapLeaf?],
|
||||
itemsJson: Array<JsonObject>,
|
||||
): Hash256 {
|
||||
const map = new ShaMap()
|
||||
itemsJson.forEach((item) => map.addItem(...itemizer(item)))
|
||||
return map.hash()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing a transaction item
|
||||
*/
|
||||
interface transactionItemObject extends JsonObject {
|
||||
hash: string
|
||||
metaData: JsonObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a transaction into an index and an item
|
||||
*
|
||||
* @param json transaction with metadata
|
||||
* @returns a tuple of index and item to be added to SHAMap
|
||||
*/
|
||||
function transactionItemizer(
|
||||
json: transactionItemObject,
|
||||
): [Hash256, ShaMapNode, undefined] {
|
||||
if (!json.hash) {
|
||||
throw new Error()
|
||||
}
|
||||
const index = Hash256.from(json.hash)
|
||||
const item = {
|
||||
hashPrefix() {
|
||||
return HashPrefix.transaction
|
||||
},
|
||||
toBytesSink(sink) {
|
||||
const serializer = new BinarySerializer(sink)
|
||||
serializer.writeLengthEncoded(STObject.from(json))
|
||||
serializer.writeLengthEncoded(STObject.from(json.metaData))
|
||||
},
|
||||
} as ShaMapNode
|
||||
return [index, item, undefined]
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing an entry item
|
||||
*/
|
||||
interface entryItemObject extends JsonObject {
|
||||
index: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an entry to a pair Hash256 and ShaMapNode
|
||||
*
|
||||
* @param json JSON describing a ledger entry item
|
||||
* @returns a tuple of index and item to be added to SHAMap
|
||||
*/
|
||||
function entryItemizer(
|
||||
json: entryItemObject,
|
||||
): [Hash256, ShaMapNode, undefined] {
|
||||
const index = Hash256.from(json.index)
|
||||
const bytes = serializeObject(json)
|
||||
const item = {
|
||||
hashPrefix() {
|
||||
return HashPrefix.accountStateEntry
|
||||
},
|
||||
toBytesSink(sink) {
|
||||
sink.put(bytes)
|
||||
},
|
||||
} as ShaMapNode
|
||||
return [index, item, undefined]
|
||||
}
|
||||
|
||||
/**
|
||||
* Function computing the hash of a transaction tree
|
||||
*
|
||||
* @param param An array of transaction objects to hash
|
||||
* @returns A Hash256 object
|
||||
*/
|
||||
function transactionTreeHash(param: Array<JsonObject>): Hash256 {
|
||||
const itemizer = transactionItemizer as (
|
||||
json: JsonObject,
|
||||
) => [Hash256, ShaMapNode, undefined]
|
||||
return computeHash(itemizer, param)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function computing the hash of accountState
|
||||
*
|
||||
* @param param A list of accountStates hash
|
||||
* @returns A Hash256 object
|
||||
*/
|
||||
function accountStateHash(param: Array<JsonObject>): Hash256 {
|
||||
const itemizer = entryItemizer as (
|
||||
json: JsonObject,
|
||||
) => [Hash256, ShaMapNode, undefined]
|
||||
return computeHash(itemizer, param)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing a ledger header
|
||||
*/
|
||||
interface ledgerObject {
|
||||
ledger_index: number
|
||||
total_coins: string | number | bigint
|
||||
parent_hash: string
|
||||
transaction_hash: string
|
||||
account_hash: string
|
||||
parent_close_time: number
|
||||
close_time: number
|
||||
close_time_resolution: number
|
||||
close_flags: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and hash a ledger header
|
||||
*
|
||||
* @param header a ledger header
|
||||
* @returns the hash of header
|
||||
*/
|
||||
function ledgerHash(header: ledgerObject): Hash256 {
|
||||
const hash = new Sha512Half()
|
||||
hash.put(HashPrefix.ledgerHeader)
|
||||
if (
|
||||
header.parent_close_time === undefined ||
|
||||
header.close_flags === undefined
|
||||
) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
UInt32.from<number>(header.ledger_index).toBytesSink(hash)
|
||||
UInt64.from<bigint>(BigInt(String(header.total_coins))).toBytesSink(hash)
|
||||
Hash256.from<string>(header.parent_hash).toBytesSink(hash)
|
||||
Hash256.from<string>(header.transaction_hash).toBytesSink(hash)
|
||||
Hash256.from<string>(header.account_hash).toBytesSink(hash)
|
||||
UInt32.from<number>(header.parent_close_time).toBytesSink(hash)
|
||||
UInt32.from<number>(header.close_time).toBytesSink(hash)
|
||||
UInt8.from<number>(header.close_time_resolution).toBytesSink(hash)
|
||||
UInt8.from<number>(header.close_flags).toBytesSink(hash)
|
||||
return hash.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a serialized ledger header
|
||||
*
|
||||
* @param binary A serialized ledger header
|
||||
* @param definitions Type definitions to parse the ledger objects.
|
||||
* Used if there are non-default ledger objects to decode.
|
||||
* @returns A JSON object describing a ledger header
|
||||
*/
|
||||
function decodeLedgerData(
|
||||
binary: string,
|
||||
definitions?: XrplDefinitionsBase,
|
||||
): object {
|
||||
if (typeof binary !== 'string') {
|
||||
throw new Error('binary must be a hex string')
|
||||
}
|
||||
const parser = new BinaryParser(binary, definitions)
|
||||
return {
|
||||
ledger_index: parser.readUInt32(),
|
||||
total_coins: parser.readType(UInt64).valueOf().toString(),
|
||||
parent_hash: parser.readType(Hash256).toHex(),
|
||||
transaction_hash: parser.readType(Hash256).toHex(),
|
||||
account_hash: parser.readType(Hash256).toHex(),
|
||||
parent_close_time: parser.readUInt32(),
|
||||
close_time: parser.readUInt32(),
|
||||
close_time_resolution: parser.readUInt8(),
|
||||
close_flags: parser.readUInt8(),
|
||||
}
|
||||
}
|
||||
|
||||
export { accountStateHash, transactionTreeHash, ledgerHash, decodeLedgerData }
|
||||
38
packages/xahau-binary-codec/src/quality.ts
Normal file
38
packages/xahau-binary-codec/src/quality.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { coreTypes } from './types'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { bytesToHex, hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* class for encoding and decoding quality
|
||||
*/
|
||||
class quality {
|
||||
/**
|
||||
* Encode quality amount
|
||||
*
|
||||
* @param arg string representation of an amount
|
||||
* @returns Serialized quality
|
||||
*/
|
||||
static encode(quality: string): Uint8Array {
|
||||
const decimal = BigNumber(quality)
|
||||
const exponent = (decimal?.e || 0) - 15
|
||||
const qualityString = decimal.times(`1e${-exponent}`).abs().toString()
|
||||
const bytes = coreTypes.UInt64.from(BigInt(qualityString)).toBytes()
|
||||
bytes[0] = exponent + 100
|
||||
return bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode quality amount
|
||||
*
|
||||
* @param arg hex-string denoting serialized quality
|
||||
* @returns deserialized quality
|
||||
*/
|
||||
static decode(quality: string): BigNumber {
|
||||
const bytes = hexToBytes(quality).slice(-8)
|
||||
const exponent = bytes[0] - 100
|
||||
const mantissa = new BigNumber(`0x${bytesToHex(bytes.slice(1))}`)
|
||||
return mantissa.times(`1e${exponent}`)
|
||||
}
|
||||
}
|
||||
|
||||
export { quality }
|
||||
228
packages/xahau-binary-codec/src/serdes/binary-parser.ts
Normal file
228
packages/xahau-binary-codec/src/serdes/binary-parser.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
XrplDefinitionsBase,
|
||||
DEFAULT_DEFINITIONS,
|
||||
FieldInstance,
|
||||
} from '../enums'
|
||||
import { type SerializedType } from '../types/serialized-type'
|
||||
import { hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* BinaryParser is used to compute fields and values from a HexString
|
||||
*/
|
||||
class BinaryParser {
|
||||
private bytes: Uint8Array
|
||||
definitions: XrplDefinitionsBase
|
||||
|
||||
/**
|
||||
* Initialize bytes to a hex string
|
||||
*
|
||||
* @param hexBytes a hex string
|
||||
* @param definitions Rippled definitions used to parse the values of transaction types and such.
|
||||
* Can be customized for sidechains and amendments.
|
||||
*/
|
||||
constructor(
|
||||
hexBytes: string,
|
||||
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
|
||||
) {
|
||||
this.bytes = hexToBytes(hexBytes)
|
||||
this.definitions = definitions
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek the first byte of the BinaryParser
|
||||
*
|
||||
* @returns The first byte of the BinaryParser
|
||||
*/
|
||||
peek(): number {
|
||||
if (this.bytes.byteLength === 0) {
|
||||
throw new Error()
|
||||
}
|
||||
return this.bytes[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the first n bytes of the BinaryParser
|
||||
*
|
||||
* @param n the number of bytes to skip
|
||||
*/
|
||||
skip(n: number): void {
|
||||
if (n > this.bytes.byteLength) {
|
||||
throw new Error()
|
||||
}
|
||||
this.bytes = this.bytes.slice(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the first n bytes from the BinaryParser
|
||||
*
|
||||
* @param n The number of bytes to read
|
||||
* @return The bytes
|
||||
*/
|
||||
read(n: number): Uint8Array {
|
||||
if (n > this.bytes.byteLength) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
const slice = this.bytes.slice(0, n)
|
||||
this.skip(n)
|
||||
return slice
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an integer of given size
|
||||
*
|
||||
* @param n The number of bytes to read
|
||||
* @return The number represented by those bytes
|
||||
*/
|
||||
readUIntN(n: number): number {
|
||||
if (0 >= n || n > 4) {
|
||||
throw new Error('invalid n')
|
||||
}
|
||||
return this.read(n).reduce((a, b) => (a << 8) | b) >>> 0
|
||||
}
|
||||
|
||||
readUInt8(): number {
|
||||
return this.readUIntN(1)
|
||||
}
|
||||
|
||||
readUInt16(): number {
|
||||
return this.readUIntN(2)
|
||||
}
|
||||
|
||||
readUInt32(): number {
|
||||
return this.readUIntN(4)
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.bytes.byteLength
|
||||
}
|
||||
|
||||
end(customEnd?: number): boolean {
|
||||
const length = this.bytes.byteLength
|
||||
return length === 0 || (customEnd !== undefined && length <= customEnd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads variable length encoded bytes
|
||||
*
|
||||
* @return The variable length bytes
|
||||
*/
|
||||
readVariableLength(): Uint8Array {
|
||||
return this.read(this.readVariableLengthLength())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the length of the variable length encoded bytes
|
||||
*
|
||||
* @return The length of the variable length encoded bytes
|
||||
*/
|
||||
readVariableLengthLength(): number {
|
||||
const b1 = this.readUInt8()
|
||||
if (b1 <= 192) {
|
||||
return b1
|
||||
} else if (b1 <= 240) {
|
||||
const b2 = this.readUInt8()
|
||||
return 193 + (b1 - 193) * 256 + b2
|
||||
} else if (b1 <= 254) {
|
||||
const b2 = this.readUInt8()
|
||||
const b3 = this.readUInt8()
|
||||
return 12481 + (b1 - 241) * 65536 + b2 * 256 + b3
|
||||
}
|
||||
throw new Error('Invalid variable length indicator')
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the field ordinal from the BinaryParser
|
||||
*
|
||||
* @return Field ordinal
|
||||
*/
|
||||
readFieldOrdinal(): number {
|
||||
let type = this.readUInt8()
|
||||
let nth = type & 15
|
||||
type >>= 4
|
||||
|
||||
if (type === 0) {
|
||||
type = this.readUInt8()
|
||||
if (type === 0 || type < 16) {
|
||||
throw new Error(
|
||||
`Cannot read FieldOrdinal, type_code ${type} out of range`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (nth === 0) {
|
||||
nth = this.readUInt8()
|
||||
if (nth === 0 || nth < 16) {
|
||||
throw new Error(
|
||||
`Cannot read FieldOrdinal, field_code ${nth} out of range`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (type << 16) | nth
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the field from the BinaryParser
|
||||
*
|
||||
* @return The field represented by the bytes at the head of the BinaryParser
|
||||
*/
|
||||
readField(): FieldInstance {
|
||||
return this.definitions.field.fromString(this.readFieldOrdinal().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a given type from the BinaryParser
|
||||
*
|
||||
* @param type The type that you want to read from the BinaryParser
|
||||
* @return The instance of that type read from the BinaryParser
|
||||
*/
|
||||
readType(type: typeof SerializedType): SerializedType {
|
||||
return type.fromParser(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type associated with a given field
|
||||
*
|
||||
* @param field The field that you wan to get the type of
|
||||
* @return The type associated with the given field
|
||||
*/
|
||||
typeForField(field: FieldInstance): typeof SerializedType {
|
||||
return field.associatedType
|
||||
}
|
||||
|
||||
/**
|
||||
* Read value of the type specified by field from the BinaryParser
|
||||
*
|
||||
* @param field The field that you want to get the associated value for
|
||||
* @return The value associated with the given field
|
||||
*/
|
||||
readFieldValue(field: FieldInstance): SerializedType {
|
||||
const type = this.typeForField(field)
|
||||
if (!type) {
|
||||
throw new Error(`unsupported: (${field.name}, ${field.type.name})`)
|
||||
}
|
||||
const sizeHint = field.isVariableLengthEncoded
|
||||
? this.readVariableLengthLength()
|
||||
: undefined
|
||||
const value = type.fromParser(this, sizeHint)
|
||||
if (value === undefined) {
|
||||
throw new Error(
|
||||
`fromParser for (${field.name}, ${field.type.name}) -> undefined `,
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next field and value from the BinaryParser
|
||||
*
|
||||
* @return The field and value
|
||||
*/
|
||||
readFieldAndValue(): [FieldInstance, SerializedType] {
|
||||
const field = this.readField()
|
||||
return [field, this.readFieldValue(field)]
|
||||
}
|
||||
}
|
||||
|
||||
export { BinaryParser }
|
||||
166
packages/xahau-binary-codec/src/serdes/binary-serializer.ts
Normal file
166
packages/xahau-binary-codec/src/serdes/binary-serializer.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { FieldInstance } from '../enums'
|
||||
import { type SerializedType } from '../types/serialized-type'
|
||||
import { bytesToHex, concat } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* Bytes list is a collection of Uint8Array objects
|
||||
*/
|
||||
class BytesList {
|
||||
private bytesArray: Array<Uint8Array> = []
|
||||
|
||||
/**
|
||||
* Get the total number of bytes in the BytesList
|
||||
*
|
||||
* @return the number of bytes
|
||||
*/
|
||||
public getLength(): number {
|
||||
return concat(this.bytesArray).byteLength
|
||||
}
|
||||
|
||||
/**
|
||||
* Put bytes in the BytesList
|
||||
*
|
||||
* @param bytesArg A Uint8Array
|
||||
* @return this BytesList
|
||||
*/
|
||||
public put(bytesArg: Uint8Array): BytesList {
|
||||
const bytes = Uint8Array.from(bytesArg) // Temporary, to catch instances of Uint8Array being passed in
|
||||
this.bytesArray.push(bytes)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this BytesList to the back of another bytes list
|
||||
*
|
||||
* @param list The BytesList to write to
|
||||
*/
|
||||
public toBytesSink(list: BytesList): void {
|
||||
list.put(this.toBytes())
|
||||
}
|
||||
|
||||
public toBytes(): Uint8Array {
|
||||
return concat(this.bytesArray)
|
||||
}
|
||||
|
||||
toHex(): string {
|
||||
return bytesToHex(this.toBytes())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BinarySerializer is used to write fields and values to Uint8Arrays
|
||||
*/
|
||||
class BinarySerializer {
|
||||
private sink: BytesList = new BytesList()
|
||||
|
||||
constructor(sink: BytesList) {
|
||||
this.sink = sink
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to this BinarySerializer
|
||||
*
|
||||
* @param value a SerializedType value
|
||||
*/
|
||||
write(value: SerializedType): void {
|
||||
value.toBytesSink(this.sink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write bytes to this BinarySerializer
|
||||
*
|
||||
* @param bytes the bytes to write
|
||||
*/
|
||||
put(bytes: Uint8Array): void {
|
||||
this.sink.put(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value of a given type to this BinarySerializer
|
||||
*
|
||||
* @param type the type to write
|
||||
* @param value a value of that type
|
||||
*/
|
||||
writeType(type: typeof SerializedType, value: SerializedType): void {
|
||||
this.write(type.from(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Write BytesList to this BinarySerializer
|
||||
*
|
||||
* @param bl BytesList to write to BinarySerializer
|
||||
*/
|
||||
writeBytesList(bl: BytesList): void {
|
||||
bl.toBytesSink(this.sink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the header of Variable Length encoded bytes
|
||||
*
|
||||
* @param length the length of the bytes
|
||||
*/
|
||||
private encodeVariableLength(length: number): Uint8Array {
|
||||
const lenBytes = new Uint8Array(3)
|
||||
if (length <= 192) {
|
||||
lenBytes[0] = length
|
||||
return lenBytes.slice(0, 1)
|
||||
} else if (length <= 12480) {
|
||||
length -= 193
|
||||
lenBytes[0] = 193 + (length >>> 8)
|
||||
lenBytes[1] = length & 0xff
|
||||
return lenBytes.slice(0, 2)
|
||||
} else if (length <= 918744) {
|
||||
length -= 12481
|
||||
lenBytes[0] = 241 + (length >>> 16)
|
||||
lenBytes[1] = (length >> 8) & 0xff
|
||||
lenBytes[2] = length & 0xff
|
||||
return lenBytes.slice(0, 3)
|
||||
}
|
||||
throw new Error('Overflow error')
|
||||
}
|
||||
|
||||
/**
|
||||
* Write field and value to BinarySerializer
|
||||
*
|
||||
* @param field field to write to BinarySerializer
|
||||
* @param value value to write to BinarySerializer
|
||||
*/
|
||||
writeFieldAndValue(
|
||||
field: FieldInstance,
|
||||
value: SerializedType,
|
||||
isUnlModifyWorkaround = false,
|
||||
): void {
|
||||
const associatedValue = field.associatedType.from(value)
|
||||
if (associatedValue.toBytesSink === undefined || field.name === undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
this.sink.put(field.header)
|
||||
|
||||
if (field.isVariableLengthEncoded) {
|
||||
this.writeLengthEncoded(associatedValue, isUnlModifyWorkaround)
|
||||
} else {
|
||||
associatedValue.toBytesSink(this.sink)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length encoded value to the BinarySerializer
|
||||
*
|
||||
* @param value length encoded value to write to BytesList
|
||||
*/
|
||||
public writeLengthEncoded(
|
||||
value: SerializedType,
|
||||
isUnlModifyWorkaround = false,
|
||||
): void {
|
||||
const bytes = new BytesList()
|
||||
if (!isUnlModifyWorkaround) {
|
||||
// this part doesn't happen for the Account field in a UNLModify transaction
|
||||
value.toBytesSink(bytes)
|
||||
}
|
||||
this.put(this.encodeVariableLength(bytes.getLength()))
|
||||
this.writeBytesList(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export { BytesList, BinarySerializer }
|
||||
186
packages/xahau-binary-codec/src/shamap.ts
Normal file
186
packages/xahau-binary-codec/src/shamap.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { coreTypes } from './types'
|
||||
import { HashPrefix } from './hash-prefixes'
|
||||
import { Sha512Half } from './hashes'
|
||||
import { Hash256 } from './types/hash-256'
|
||||
import { BytesList } from './serdes/binary-serializer'
|
||||
|
||||
/**
|
||||
* Abstract class describing a SHAMapNode
|
||||
*/
|
||||
abstract class ShaMapNode {
|
||||
abstract hashPrefix(): Uint8Array
|
||||
abstract isLeaf(): boolean
|
||||
abstract isInner(): boolean
|
||||
abstract toBytesSink(list: BytesList): void
|
||||
abstract hash(): Hash256
|
||||
}
|
||||
|
||||
/**
|
||||
* Class describing a Leaf of SHAMap
|
||||
*/
|
||||
class ShaMapLeaf extends ShaMapNode {
|
||||
constructor(public index: Hash256, public item?: ShaMapNode) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true as ShaMapLeaf is a leaf node
|
||||
*/
|
||||
isLeaf(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns false as ShaMapLeaf is not an inner node
|
||||
*/
|
||||
isInner(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix of the this.item
|
||||
*
|
||||
* @returns The hash prefix, unless this.item is undefined, then it returns an empty Uint8Array
|
||||
*/
|
||||
hashPrefix(): Uint8Array {
|
||||
return this.item === undefined ? new Uint8Array(0) : this.item.hashPrefix()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the bytes representation of this
|
||||
*
|
||||
* @returns hash of this.item concatenated with this.index
|
||||
*/
|
||||
hash(): Hash256 {
|
||||
const hash = Sha512Half.put(this.hashPrefix())
|
||||
this.toBytesSink(hash)
|
||||
return hash.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the bytes representation of this to a BytesList
|
||||
* @param list BytesList to write bytes to
|
||||
*/
|
||||
toBytesSink(list: BytesList): void {
|
||||
if (this.item !== undefined) {
|
||||
this.item.toBytesSink(list)
|
||||
}
|
||||
this.index.toBytesSink(list)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class defining an Inner Node of a SHAMap
|
||||
*/
|
||||
class ShaMapInner extends ShaMapNode {
|
||||
private slotBits = 0
|
||||
private branches: Array<ShaMapNode> = Array(16)
|
||||
|
||||
constructor(private depth: number = 0) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true as ShaMapInner is an inner node
|
||||
*/
|
||||
isInner(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns false as ShaMapInner is not a leaf node
|
||||
*/
|
||||
isLeaf(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash prefix for this node
|
||||
*
|
||||
* @returns hash prefix describing an inner node
|
||||
*/
|
||||
hashPrefix(): Uint8Array {
|
||||
return HashPrefix.innerNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a branch of this node to be another node
|
||||
*
|
||||
* @param slot Slot to add branch to this.branches
|
||||
* @param branch Branch to add
|
||||
*/
|
||||
setBranch(slot: number, branch: ShaMapNode): void {
|
||||
this.slotBits = this.slotBits | (1 << slot)
|
||||
this.branches[slot] = branch
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if node is empty
|
||||
*/
|
||||
empty(): boolean {
|
||||
return this.slotBits === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of this node
|
||||
*
|
||||
* @returns The hash of this node
|
||||
*/
|
||||
hash(): Hash256 {
|
||||
if (this.empty()) {
|
||||
return (coreTypes.Hash256 as typeof Hash256).ZERO_256
|
||||
}
|
||||
const hash = Sha512Half.put(this.hashPrefix())
|
||||
this.toBytesSink(hash)
|
||||
return hash.finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the bytes representation of this node to a BytesList
|
||||
*
|
||||
* @param list BytesList to write bytes to
|
||||
*/
|
||||
toBytesSink(list: BytesList): void {
|
||||
for (let i = 0; i < this.branches.length; i++) {
|
||||
const branch = this.branches[i]
|
||||
const hash = branch
|
||||
? branch.hash()
|
||||
: (coreTypes.Hash256 as typeof Hash256).ZERO_256
|
||||
hash.toBytesSink(list)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to the SHAMap
|
||||
*
|
||||
* @param index Hash of the index of the item being inserted
|
||||
* @param item Item to insert in the map
|
||||
* @param leaf Leaf node to insert when branch doesn't exist
|
||||
*/
|
||||
addItem(index?: Hash256, item?: ShaMapNode, leaf?: ShaMapLeaf): void {
|
||||
if (index === undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
if (index !== undefined) {
|
||||
const nibble = index.nibblet(this.depth)
|
||||
const existing = this.branches[nibble]
|
||||
|
||||
if (existing === undefined) {
|
||||
this.setBranch(nibble, leaf || new ShaMapLeaf(index, item))
|
||||
} else if (existing instanceof ShaMapLeaf) {
|
||||
const newInner = new ShaMapInner(this.depth + 1)
|
||||
newInner.addItem(existing.index, undefined, existing)
|
||||
newInner.addItem(index, item, leaf)
|
||||
this.setBranch(nibble, newInner)
|
||||
} else if (existing instanceof ShaMapInner) {
|
||||
existing.addItem(index, item, leaf)
|
||||
} else {
|
||||
throw new Error('invalid ShaMap.addItem call')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShaMap extends ShaMapInner {}
|
||||
|
||||
export { ShaMap, ShaMapNode, ShaMapLeaf }
|
||||
86
packages/xahau-binary-codec/src/types/account-id.ts
Normal file
86
packages/xahau-binary-codec/src/types/account-id.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
decodeAccountID,
|
||||
encodeAccountID,
|
||||
isValidXAddress,
|
||||
xAddressToClassicAddress,
|
||||
} from 'xahau-address-codec'
|
||||
import { Hash160 } from './hash-160'
|
||||
import { hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
const HEX_REGEX = /^[A-F0-9]{40}$/
|
||||
|
||||
/**
|
||||
* Class defining how to encode and decode an AccountID
|
||||
*/
|
||||
class AccountID extends Hash160 {
|
||||
static readonly defaultAccountID: AccountID = new AccountID(
|
||||
new Uint8Array(20),
|
||||
)
|
||||
|
||||
constructor(bytes?: Uint8Array) {
|
||||
super(bytes ?? AccountID.defaultAccountID.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to construct an AccountID
|
||||
*
|
||||
* @param value either an existing AccountID, a hex-string, or a base58 r-Address
|
||||
* @returns an AccountID object
|
||||
*/
|
||||
static from<T extends Hash160 | string>(value: T): AccountID {
|
||||
if (value instanceof AccountID) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (value === '') {
|
||||
return new AccountID()
|
||||
}
|
||||
|
||||
return HEX_REGEX.test(value)
|
||||
? new AccountID(hexToBytes(value))
|
||||
: this.fromBase58(value)
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct AccountID from value given')
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to build an AccountID from a base58 r-Address
|
||||
*
|
||||
* @param value a base58 r-Address
|
||||
* @returns an AccountID object
|
||||
*/
|
||||
static fromBase58(value: string): AccountID {
|
||||
if (isValidXAddress(value)) {
|
||||
const classic = xAddressToClassicAddress(value)
|
||||
|
||||
if (classic.tag !== false)
|
||||
throw new Error('Only allowed to have tag on Account or Destination')
|
||||
|
||||
value = classic.classicAddress
|
||||
}
|
||||
|
||||
return new AccountID(Uint8Array.from(decodeAccountID(value)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload of toJSON
|
||||
*
|
||||
* @returns the base58 string for this AccountID
|
||||
*/
|
||||
toJSON(): string {
|
||||
return this.toBase58()
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to encode AccountID into a base58 address
|
||||
*
|
||||
* @returns the base58 string defined by this.bytes
|
||||
*/
|
||||
toBase58(): string {
|
||||
return encodeAccountID(this.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export { AccountID }
|
||||
255
packages/xahau-binary-codec/src/types/amount.ts
Normal file
255
packages/xahau-binary-codec/src/types/amount.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
|
||||
import { AccountID } from './account-id'
|
||||
import { Currency } from './currency'
|
||||
import { JsonObject, SerializedType } from './serialized-type'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
import { readUInt32BE, writeUInt32BE } from '../utils'
|
||||
|
||||
/**
|
||||
* Constants for validating amounts
|
||||
*/
|
||||
const MIN_IOU_EXPONENT = -96
|
||||
const MAX_IOU_EXPONENT = 80
|
||||
const MAX_IOU_PRECISION = 16
|
||||
const MAX_DROPS = new BigNumber('1e17')
|
||||
const MIN_XAH = new BigNumber('1e-6')
|
||||
const mask = BigInt(0x00000000ffffffff)
|
||||
|
||||
/**
|
||||
* BigNumber configuration for Amount IOUs
|
||||
*/
|
||||
BigNumber.config({
|
||||
EXPONENTIAL_AT: [
|
||||
MIN_IOU_EXPONENT - MAX_IOU_PRECISION,
|
||||
MAX_IOU_EXPONENT + MAX_IOU_PRECISION,
|
||||
],
|
||||
})
|
||||
|
||||
/**
|
||||
* Interface for JSON objects that represent amounts
|
||||
*/
|
||||
interface AmountObject extends JsonObject {
|
||||
value: string
|
||||
currency: string
|
||||
issuer: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for AmountObject
|
||||
*/
|
||||
function isAmountObject(arg): arg is AmountObject {
|
||||
const keys = Object.keys(arg).sort()
|
||||
return (
|
||||
keys.length === 3 &&
|
||||
keys[0] === 'currency' &&
|
||||
keys[1] === 'issuer' &&
|
||||
keys[2] === 'value'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing/Deserializing Amounts
|
||||
*/
|
||||
class Amount extends SerializedType {
|
||||
static defaultAmount: Amount = new Amount(hexToBytes('4000000000000000'))
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? Amount.defaultAmount.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an amount from an IOU or string amount
|
||||
*
|
||||
* @param value An Amount, object representing an IOU, or a string
|
||||
* representing an integer amount
|
||||
* @returns An Amount object
|
||||
*/
|
||||
static from<T extends Amount | AmountObject | string>(value: T): Amount {
|
||||
if (value instanceof Amount) {
|
||||
return value
|
||||
}
|
||||
|
||||
let amount = new Uint8Array(8)
|
||||
if (typeof value === 'string') {
|
||||
Amount.assertXrpIsValid(value)
|
||||
|
||||
const number = BigInt(value)
|
||||
|
||||
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
|
||||
writeUInt32BE(intBuf[0], Number(number >> BigInt(32)), 0)
|
||||
writeUInt32BE(intBuf[1], Number(number & BigInt(mask)), 0)
|
||||
|
||||
amount = concat(intBuf)
|
||||
|
||||
amount[0] |= 0x40
|
||||
|
||||
return new Amount(amount)
|
||||
}
|
||||
|
||||
if (isAmountObject(value)) {
|
||||
const number = new BigNumber(value.value)
|
||||
Amount.assertIouIsValid(number)
|
||||
|
||||
if (number.isZero()) {
|
||||
amount[0] |= 0x80
|
||||
} else {
|
||||
const integerNumberString = number
|
||||
.times(`1e${-((number.e || 0) - 15)}`)
|
||||
.abs()
|
||||
.toString()
|
||||
|
||||
const num = BigInt(integerNumberString)
|
||||
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
|
||||
writeUInt32BE(intBuf[0], Number(num >> BigInt(32)), 0)
|
||||
writeUInt32BE(intBuf[1], Number(num & BigInt(mask)), 0)
|
||||
|
||||
amount = concat(intBuf)
|
||||
|
||||
amount[0] |= 0x80
|
||||
|
||||
if (number.gt(new BigNumber(0))) {
|
||||
amount[0] |= 0x40
|
||||
}
|
||||
|
||||
const exponent = (number.e || 0) - 15
|
||||
const exponentByte = 97 + exponent
|
||||
amount[0] |= exponentByte >>> 2
|
||||
amount[1] |= (exponentByte & 0x03) << 6
|
||||
}
|
||||
|
||||
const currency = Currency.from(value.currency).toBytes()
|
||||
const issuer = AccountID.from(value.issuer).toBytes()
|
||||
return new Amount(concat([amount, currency, issuer]))
|
||||
}
|
||||
|
||||
throw new Error('Invalid type to construct an Amount')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an amount from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read the Amount from
|
||||
* @returns An Amount object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): Amount {
|
||||
const isXAH = parser.peek() & 0x80
|
||||
const numBytes = isXAH ? 48 : 8
|
||||
return new Amount(parser.read(numBytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of this Amount
|
||||
*
|
||||
* @returns the JSON interpretation of this.bytes
|
||||
*/
|
||||
toJSON(): AmountObject | string {
|
||||
if (this.isNative()) {
|
||||
const bytes = this.bytes
|
||||
const isPositive = bytes[0] & 0x40
|
||||
const sign = isPositive ? '' : '-'
|
||||
bytes[0] &= 0x3f
|
||||
|
||||
const msb = BigInt(readUInt32BE(bytes.slice(0, 4), 0))
|
||||
const lsb = BigInt(readUInt32BE(bytes.slice(4), 0))
|
||||
const num = (msb << BigInt(32)) | lsb
|
||||
|
||||
return `${sign}${num.toString()}`
|
||||
} else {
|
||||
const parser = new BinaryParser(this.toString())
|
||||
const mantissa = parser.read(8)
|
||||
const currency = Currency.fromParser(parser) as Currency
|
||||
const issuer = AccountID.fromParser(parser) as AccountID
|
||||
|
||||
const b1 = mantissa[0]
|
||||
const b2 = mantissa[1]
|
||||
|
||||
const isPositive = b1 & 0x40
|
||||
const sign = isPositive ? '' : '-'
|
||||
const exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97
|
||||
|
||||
mantissa[0] = 0
|
||||
mantissa[1] &= 0x3f
|
||||
const value = new BigNumber(`${sign}0x${bytesToHex(mantissa)}`).times(
|
||||
`1e${exponent}`,
|
||||
)
|
||||
Amount.assertIouIsValid(value)
|
||||
|
||||
return {
|
||||
value: value.toString(),
|
||||
currency: currency.toJSON(),
|
||||
issuer: issuer.toJSON(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate XAH amount
|
||||
*
|
||||
* @param amount String representing XAH amount
|
||||
* @returns void, but will throw if invalid amount
|
||||
*/
|
||||
private static assertXrpIsValid(amount: string): void {
|
||||
if (amount.indexOf('.') !== -1) {
|
||||
throw new Error(`${amount.toString()} is an illegal amount`)
|
||||
}
|
||||
|
||||
const decimal = new BigNumber(amount)
|
||||
if (!decimal.isZero()) {
|
||||
if (decimal.lt(MIN_XAH) || decimal.gt(MAX_DROPS)) {
|
||||
throw new Error(`${amount.toString()} is an illegal amount`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IOU.value amount
|
||||
*
|
||||
* @param decimal BigNumber object representing IOU.value
|
||||
* @returns void, but will throw if invalid amount
|
||||
*/
|
||||
private static assertIouIsValid(decimal: BigNumber): void {
|
||||
if (!decimal.isZero()) {
|
||||
const p = decimal.precision()
|
||||
const e = (decimal.e || 0) - 15
|
||||
if (
|
||||
p > MAX_IOU_PRECISION ||
|
||||
e > MAX_IOU_EXPONENT ||
|
||||
e < MIN_IOU_EXPONENT
|
||||
) {
|
||||
throw new Error('Decimal precision out of range')
|
||||
}
|
||||
this.verifyNoDecimal(decimal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the value after being multiplied by the exponent does not
|
||||
* contain a decimal.
|
||||
*
|
||||
* @param decimal a Decimal object
|
||||
* @returns a string of the object without a decimal
|
||||
*/
|
||||
private static verifyNoDecimal(decimal: BigNumber): void {
|
||||
const integerNumberString = decimal
|
||||
.times(`1e${-((decimal.e || 0) - 15)}`)
|
||||
.abs()
|
||||
.toString()
|
||||
|
||||
if (integerNumberString.indexOf('.') !== -1) {
|
||||
throw new Error('Decimal place found in integerNumberString')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if this amount is in units of Native Currency(XAH)
|
||||
*
|
||||
* @returns true if Native (XAH)
|
||||
*/
|
||||
private isNative(): boolean {
|
||||
return (this.bytes[0] & 0x80) === 0
|
||||
}
|
||||
}
|
||||
|
||||
export { Amount, AmountObject }
|
||||
46
packages/xahau-binary-codec/src/types/blob.ts
Normal file
46
packages/xahau-binary-codec/src/types/blob.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { SerializedType } from './serialized-type'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* Variable length encoded type
|
||||
*/
|
||||
class Blob extends SerializedType {
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how to read a Blob from a BinaryParser
|
||||
*
|
||||
* @param parser The binary parser to read the Blob from
|
||||
* @param hint The length of the blob, computed by readVariableLengthLength() and passed in
|
||||
* @returns A Blob object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser, hint: number): Blob {
|
||||
return new Blob(parser.read(hint))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Blob object from a hex-string
|
||||
*
|
||||
* @param value existing Blob object or a hex-string
|
||||
* @returns A Blob object
|
||||
*/
|
||||
static from<T extends Blob | string>(value: T): Blob {
|
||||
if (value instanceof Blob) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (!/^[A-F0-9]*$/iu.test(value)) {
|
||||
throw new Error('Cannot construct Blob from a non-hex string')
|
||||
}
|
||||
return new Blob(hexToBytes(value))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct Blob from value given')
|
||||
}
|
||||
}
|
||||
|
||||
export { Blob }
|
||||
140
packages/xahau-binary-codec/src/types/currency.ts
Normal file
140
packages/xahau-binary-codec/src/types/currency.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Hash160 } from './hash-160'
|
||||
import { bytesToHex, hexToBytes, hexToString } from '@xrplf/isomorphic/utils'
|
||||
|
||||
const XAH_HEX_REGEX = /^0{40}$/
|
||||
const ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}[\]|]{3}$/
|
||||
const HEX_REGEX = /^[A-F0-9]{40}$/
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const STANDARD_FORMAT_HEX_REGEX = /^0{24}[\x00-\x7F]{6}0{10}$/
|
||||
|
||||
/**
|
||||
* Convert an ISO code to a currency bytes representation
|
||||
*/
|
||||
function isoToBytes(iso: string): Uint8Array {
|
||||
const bytes = new Uint8Array(20)
|
||||
if (iso !== 'XAH') {
|
||||
const isoBytes = iso.split('').map((c) => c.charCodeAt(0))
|
||||
bytes.set(isoBytes, 12)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if ISO is a valid iso code
|
||||
*/
|
||||
function isIsoCode(iso: string): boolean {
|
||||
return ISO_REGEX.test(iso)
|
||||
}
|
||||
|
||||
function isoCodeFromHex(code: Uint8Array): string | null {
|
||||
const iso = hexToString(bytesToHex(code))
|
||||
if (iso === 'XAH') {
|
||||
return null
|
||||
}
|
||||
if (isIsoCode(iso)) {
|
||||
return iso
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if hex is a valid hex-string
|
||||
*/
|
||||
function isHex(hex: string): boolean {
|
||||
return HEX_REGEX.test(hex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a string is a valid representation of a currency
|
||||
*/
|
||||
function isStringRepresentation(input: string): boolean {
|
||||
return input.length === 3 || isHex(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a Uint8Array is a valid representation of a currency
|
||||
*/
|
||||
function isBytesArray(bytes: Uint8Array): boolean {
|
||||
return bytes.byteLength === 20
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a value is a valid representation of a currency
|
||||
*/
|
||||
function isValidRepresentation(input: Uint8Array | string): boolean {
|
||||
return input instanceof Uint8Array
|
||||
? isBytesArray(input)
|
||||
: isStringRepresentation(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate bytes from a string or UInt8Array representation of a currency
|
||||
*/
|
||||
function bytesFromRepresentation(input: string): Uint8Array {
|
||||
if (!isValidRepresentation(input)) {
|
||||
throw new Error(`Unsupported Currency representation: ${input}`)
|
||||
}
|
||||
return input.length === 3 ? isoToBytes(input) : hexToBytes(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class defining how to encode and decode Currencies
|
||||
*/
|
||||
class Currency extends Hash160 {
|
||||
static readonly XAH = new Currency(new Uint8Array(20))
|
||||
private readonly _iso: string | null
|
||||
|
||||
constructor(byteBuf: Uint8Array) {
|
||||
super(byteBuf ?? Currency.XAH.bytes)
|
||||
const hex = bytesToHex(this.bytes)
|
||||
|
||||
if (XAH_HEX_REGEX.test(hex)) {
|
||||
this._iso = 'XAH'
|
||||
} else if (STANDARD_FORMAT_HEX_REGEX.test(hex)) {
|
||||
this._iso = isoCodeFromHex(this.bytes.slice(12, 15))
|
||||
} else {
|
||||
this._iso = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ISO code of this currency
|
||||
*
|
||||
* @returns ISO code if it exists, else null
|
||||
*/
|
||||
iso(): string | null {
|
||||
return this._iso
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Currency object
|
||||
*
|
||||
* @param val Currency object or a string representation of a currency
|
||||
*/
|
||||
static from<T extends Hash160 | string>(value: T): Currency {
|
||||
if (value instanceof Currency) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return new Currency(bytesFromRepresentation(value))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct Currency from value given')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the JSON representation of a currency
|
||||
*
|
||||
* @returns JSON representation
|
||||
*/
|
||||
toJSON(): string {
|
||||
const iso = this.iso()
|
||||
if (iso !== null) {
|
||||
return iso
|
||||
}
|
||||
return bytesToHex(this.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export { Currency }
|
||||
33
packages/xahau-binary-codec/src/types/hash-128.ts
Normal file
33
packages/xahau-binary-codec/src/types/hash-128.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Hash } from './hash'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* Hash with a width of 128 bits
|
||||
*/
|
||||
class Hash128 extends Hash {
|
||||
static readonly width = 16
|
||||
static readonly ZERO_128: Hash128 = new Hash128(new Uint8Array(Hash128.width))
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
if (bytes && bytes.byteLength === 0) {
|
||||
bytes = Hash128.ZERO_128.bytes
|
||||
}
|
||||
|
||||
super(bytes ?? Hash128.ZERO_128.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hex representation of a hash-128 bytes, allowing unset
|
||||
*
|
||||
* @returns hex String of this.bytes
|
||||
*/
|
||||
toHex(): string {
|
||||
const hex = bytesToHex(this.toBytes())
|
||||
if (/^0+$/.exec(hex)) {
|
||||
return ''
|
||||
}
|
||||
return hex
|
||||
}
|
||||
}
|
||||
|
||||
export { Hash128 }
|
||||
19
packages/xahau-binary-codec/src/types/hash-160.ts
Normal file
19
packages/xahau-binary-codec/src/types/hash-160.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Hash } from './hash'
|
||||
|
||||
/**
|
||||
* Hash with a width of 160 bits
|
||||
*/
|
||||
class Hash160 extends Hash {
|
||||
static readonly width = 20
|
||||
static readonly ZERO_160: Hash160 = new Hash160(new Uint8Array(Hash160.width))
|
||||
|
||||
constructor(bytes?: Uint8Array) {
|
||||
if (bytes && bytes.byteLength === 0) {
|
||||
bytes = Hash160.ZERO_160.bytes
|
||||
}
|
||||
|
||||
super(bytes ?? Hash160.ZERO_160.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export { Hash160 }
|
||||
15
packages/xahau-binary-codec/src/types/hash-256.ts
Normal file
15
packages/xahau-binary-codec/src/types/hash-256.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Hash } from './hash'
|
||||
|
||||
/**
|
||||
* Hash with a width of 256 bits
|
||||
*/
|
||||
class Hash256 extends Hash {
|
||||
static readonly width = 32
|
||||
static readonly ZERO_256 = new Hash256(new Uint8Array(Hash256.width))
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? Hash256.ZERO_256.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
export { Hash256 }
|
||||
83
packages/xahau-binary-codec/src/types/hash.ts
Normal file
83
packages/xahau-binary-codec/src/types/hash.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Comparable } from './serialized-type'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
import { compare } from '../utils'
|
||||
|
||||
/**
|
||||
* Base class defining how to encode and decode hashes
|
||||
*/
|
||||
class Hash extends Comparable<Hash | string> {
|
||||
static readonly width: number
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes)
|
||||
if (this.bytes.length !== (this.constructor as typeof Hash).width) {
|
||||
throw new Error(`Invalid Hash length ${this.bytes.byteLength}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Hash object from an existing Hash object or a hex-string
|
||||
*
|
||||
* @param value A hash object or hex-string of a hash
|
||||
*/
|
||||
static from<T extends Hash | string>(value: T): Hash {
|
||||
if (value instanceof this) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return new this(hexToBytes(value))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct Hash from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a Hash object from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read the hash from
|
||||
* @param hint length of the bytes to read, optional
|
||||
*/
|
||||
static fromParser(parser: BinaryParser, hint?: number): Hash {
|
||||
return new this(parser.read(hint ?? this.width))
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded operator for comparing two hash objects
|
||||
*
|
||||
* @param other The Hash to compare this to
|
||||
*/
|
||||
compareTo(other: Hash): number {
|
||||
return compare(
|
||||
this.bytes,
|
||||
(this.constructor as typeof Hash).from(other).bytes,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the hex-string representation of this Hash
|
||||
*/
|
||||
toString(): string {
|
||||
return this.toHex()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns four bits at the specified depth within a hash
|
||||
*
|
||||
* @param depth The depth of the four bits
|
||||
* @returns The number represented by the four bits
|
||||
*/
|
||||
nibblet(depth: number): number {
|
||||
const byteIx = depth > 0 ? (depth / 2) | 0 : 0
|
||||
let b = this.bytes[byteIx]
|
||||
if (depth % 2 === 0) {
|
||||
b = (b & 0xf0) >>> 4
|
||||
} else {
|
||||
b = b & 0x0f
|
||||
}
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
export { Hash }
|
||||
63
packages/xahau-binary-codec/src/types/index.ts
Normal file
63
packages/xahau-binary-codec/src/types/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { AccountID } from './account-id'
|
||||
import { Amount } from './amount'
|
||||
import { Blob } from './blob'
|
||||
import { Currency } from './currency'
|
||||
import { Hash128 } from './hash-128'
|
||||
import { Hash160 } from './hash-160'
|
||||
import { Hash256 } from './hash-256'
|
||||
import { Issue } from './issue'
|
||||
import { PathSet } from './path-set'
|
||||
import { STArray } from './st-array'
|
||||
import { STObject } from './st-object'
|
||||
import { UInt16 } from './uint-16'
|
||||
import { UInt32 } from './uint-32'
|
||||
import { UInt64 } from './uint-64'
|
||||
import { UInt8 } from './uint-8'
|
||||
import { Vector256 } from './vector-256'
|
||||
import { XChainBridge } from './xchain-bridge'
|
||||
import { type SerializedType } from './serialized-type'
|
||||
import { DEFAULT_DEFINITIONS } from '../enums'
|
||||
|
||||
const coreTypes: Record<string, typeof SerializedType> = {
|
||||
AccountID,
|
||||
Amount,
|
||||
Blob,
|
||||
Currency,
|
||||
Hash128,
|
||||
Hash160,
|
||||
Hash256,
|
||||
Issue,
|
||||
PathSet,
|
||||
STArray,
|
||||
STObject,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Vector256,
|
||||
XChainBridge,
|
||||
}
|
||||
|
||||
// Ensures that the DEFAULT_DEFINITIONS object connects these types to fields for serializing/deserializing
|
||||
// This is done here instead of in enums/index.ts to avoid a circular dependency
|
||||
// because some of the above types depend on BinarySerializer which depends on enums/index.ts.
|
||||
DEFAULT_DEFINITIONS.associateTypes(coreTypes)
|
||||
|
||||
export {
|
||||
coreTypes,
|
||||
AccountID,
|
||||
Amount,
|
||||
Blob,
|
||||
Currency,
|
||||
Hash128,
|
||||
Hash160,
|
||||
Hash256,
|
||||
PathSet,
|
||||
STArray,
|
||||
STObject,
|
||||
UInt8,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Vector256,
|
||||
}
|
||||
96
packages/xahau-binary-codec/src/types/issue.ts
Normal file
96
packages/xahau-binary-codec/src/types/issue.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { concat } from '@xrplf/isomorphic/utils'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
|
||||
import { AccountID } from './account-id'
|
||||
import { Currency } from './currency'
|
||||
import { JsonObject, SerializedType } from './serialized-type'
|
||||
|
||||
/**
|
||||
* Interface for JSON objects that represent amounts
|
||||
*/
|
||||
interface IssueObject extends JsonObject {
|
||||
currency: string
|
||||
issuer?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for AmountObject
|
||||
*/
|
||||
function isIssueObject(arg): arg is IssueObject {
|
||||
const keys = Object.keys(arg).sort()
|
||||
if (keys.length === 1) {
|
||||
return keys[0] === 'currency'
|
||||
}
|
||||
return keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer'
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing/Deserializing Amounts
|
||||
*/
|
||||
class Issue extends SerializedType {
|
||||
static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(new Uint8Array(20))
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? Issue.ZERO_ISSUED_CURRENCY.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an amount from an IOU or string amount
|
||||
*
|
||||
* @param value An Amount, object representing an IOU, or a string
|
||||
* representing an integer amount
|
||||
* @returns An Amount object
|
||||
*/
|
||||
static from<T extends Issue | IssueObject>(value: T): Issue {
|
||||
if (value instanceof Issue) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (isIssueObject(value)) {
|
||||
const currency = Currency.from(value.currency).toBytes()
|
||||
if (value.issuer == null) {
|
||||
return new Issue(currency)
|
||||
}
|
||||
const issuer = AccountID.from(value.issuer).toBytes()
|
||||
return new Issue(concat([currency, issuer]))
|
||||
}
|
||||
|
||||
throw new Error('Invalid type to construct an Amount')
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an amount from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read the Amount from
|
||||
* @returns An Amount object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): Issue {
|
||||
const currency = parser.read(20)
|
||||
if (new Currency(currency).toJSON() === 'XAH') {
|
||||
return new Issue(currency)
|
||||
}
|
||||
const currencyAndIssuer = [currency, parser.read(20)]
|
||||
return new Issue(concat(currencyAndIssuer))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of this Amount
|
||||
*
|
||||
* @returns the JSON interpretation of this.bytes
|
||||
*/
|
||||
toJSON(): IssueObject {
|
||||
const parser = new BinaryParser(this.toString())
|
||||
const currency = Currency.fromParser(parser) as Currency
|
||||
if (currency.toJSON() === 'XAH') {
|
||||
return { currency: currency.toJSON() }
|
||||
}
|
||||
const issuer = AccountID.fromParser(parser) as AccountID
|
||||
|
||||
return {
|
||||
currency: currency.toJSON(),
|
||||
issuer: issuer.toJSON(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Issue, IssueObject }
|
||||
290
packages/xahau-binary-codec/src/types/path-set.ts
Normal file
290
packages/xahau-binary-codec/src/types/path-set.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { AccountID } from './account-id'
|
||||
import { Currency } from './currency'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { SerializedType, JsonObject } from './serialized-type'
|
||||
import { bytesToHex, concat } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* Constants for separating Paths in a PathSet
|
||||
*/
|
||||
const PATHSET_END_BYTE = 0x00
|
||||
const PATH_SEPARATOR_BYTE = 0xff
|
||||
|
||||
/**
|
||||
* Constant for masking types of a Hop
|
||||
*/
|
||||
const TYPE_ACCOUNT = 0x01
|
||||
const TYPE_CURRENCY = 0x10
|
||||
const TYPE_ISSUER = 0x20
|
||||
|
||||
/**
|
||||
* The object representation of a Hop, an issuer AccountID, an account AccountID, and a Currency
|
||||
*/
|
||||
interface HopObject extends JsonObject {
|
||||
issuer?: string
|
||||
account?: string
|
||||
currency?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeGuard for HopObject
|
||||
*/
|
||||
function isHopObject(arg): arg is HopObject {
|
||||
return (
|
||||
arg.issuer !== undefined ||
|
||||
arg.account !== undefined ||
|
||||
arg.currency !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeGuard for PathSet
|
||||
*/
|
||||
function isPathSet(arg): arg is Array<Array<HopObject>> {
|
||||
return (
|
||||
(Array.isArray(arg) && arg.length === 0) ||
|
||||
(Array.isArray(arg) && Array.isArray(arg[0]) && arg[0].length === 0) ||
|
||||
(Array.isArray(arg) && Array.isArray(arg[0]) && isHopObject(arg[0][0]))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and Deserialize a Hop
|
||||
*/
|
||||
class Hop extends SerializedType {
|
||||
/**
|
||||
* Create a Hop from a HopObject
|
||||
*
|
||||
* @param value Either a hop or HopObject to create a hop with
|
||||
* @returns a Hop
|
||||
*/
|
||||
static from(value: Hop | HopObject): Hop {
|
||||
if (value instanceof Hop) {
|
||||
return value
|
||||
}
|
||||
|
||||
const bytes: Array<Uint8Array> = [Uint8Array.from([0])]
|
||||
|
||||
if (value.account) {
|
||||
bytes.push(AccountID.from(value.account).toBytes())
|
||||
bytes[0][0] |= TYPE_ACCOUNT
|
||||
}
|
||||
|
||||
if (value.currency) {
|
||||
bytes.push(Currency.from(value.currency).toBytes())
|
||||
bytes[0][0] |= TYPE_CURRENCY
|
||||
}
|
||||
|
||||
if (value.issuer) {
|
||||
bytes.push(AccountID.from(value.issuer).toBytes())
|
||||
bytes[0][0] |= TYPE_ISSUER
|
||||
}
|
||||
|
||||
return new Hop(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Hop from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read the Hop from
|
||||
* @returns a Hop
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): Hop {
|
||||
const type = parser.readUInt8()
|
||||
const bytes: Array<Uint8Array> = [Uint8Array.from([type])]
|
||||
|
||||
if (type & TYPE_ACCOUNT) {
|
||||
bytes.push(parser.read(AccountID.width))
|
||||
}
|
||||
|
||||
if (type & TYPE_CURRENCY) {
|
||||
bytes.push(parser.read(Currency.width))
|
||||
}
|
||||
|
||||
if (type & TYPE_ISSUER) {
|
||||
bytes.push(parser.read(AccountID.width))
|
||||
}
|
||||
|
||||
return new Hop(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON interpretation of this hop
|
||||
*
|
||||
* @returns a HopObject, an JS object with optional account, issuer, and currency
|
||||
*/
|
||||
toJSON(): HopObject {
|
||||
const hopParser = new BinaryParser(bytesToHex(this.bytes))
|
||||
const type = hopParser.readUInt8()
|
||||
|
||||
let account, currency, issuer
|
||||
if (type & TYPE_ACCOUNT) {
|
||||
account = (AccountID.fromParser(hopParser) as AccountID).toJSON()
|
||||
}
|
||||
|
||||
if (type & TYPE_CURRENCY) {
|
||||
currency = (Currency.fromParser(hopParser) as Currency).toJSON()
|
||||
}
|
||||
|
||||
if (type & TYPE_ISSUER) {
|
||||
issuer = (AccountID.fromParser(hopParser) as AccountID).toJSON()
|
||||
}
|
||||
|
||||
const result: HopObject = {}
|
||||
if (account) {
|
||||
result.account = account
|
||||
}
|
||||
|
||||
if (issuer) {
|
||||
result.issuer = issuer
|
||||
}
|
||||
|
||||
if (currency) {
|
||||
result.currency = currency
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* get a number representing the type of this hop
|
||||
*
|
||||
* @returns a number to be bitwise and-ed with TYPE_ constants to describe the types in the hop
|
||||
*/
|
||||
type(): number {
|
||||
return this.bytes[0]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing/deserializing Paths
|
||||
*/
|
||||
class Path extends SerializedType {
|
||||
/**
|
||||
* construct a Path from an array of Hops
|
||||
*
|
||||
* @param value Path or array of HopObjects to construct a Path
|
||||
* @returns the Path
|
||||
*/
|
||||
static from(value: Path | Array<HopObject>): Path {
|
||||
if (value instanceof Path) {
|
||||
return value
|
||||
}
|
||||
|
||||
const bytes: Array<Uint8Array> = []
|
||||
value.forEach((hop: HopObject) => {
|
||||
bytes.push(Hop.from(hop).toBytes())
|
||||
})
|
||||
|
||||
return new Path(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a Path from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read Path from
|
||||
* @returns the Path represented by the bytes read from the BinaryParser
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): Path {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
while (!parser.end()) {
|
||||
bytes.push(Hop.fromParser(parser).toBytes())
|
||||
|
||||
if (
|
||||
parser.peek() === PATHSET_END_BYTE ||
|
||||
parser.peek() === PATH_SEPARATOR_BYTE
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return new Path(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of this Path
|
||||
*
|
||||
* @returns an Array of HopObject constructed from this.bytes
|
||||
*/
|
||||
toJSON(): Array<HopObject> {
|
||||
const json: Array<HopObject> = []
|
||||
const pathParser = new BinaryParser(this.toString())
|
||||
|
||||
while (!pathParser.end()) {
|
||||
json.push(Hop.fromParser(pathParser).toJSON())
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and Serialize the PathSet type
|
||||
*/
|
||||
class PathSet extends SerializedType {
|
||||
/**
|
||||
* Construct a PathSet from an Array of Arrays representing paths
|
||||
*
|
||||
* @param value A PathSet or Array of Array of HopObjects
|
||||
* @returns the PathSet constructed from value
|
||||
*/
|
||||
static from<T extends PathSet | Array<Array<HopObject>>>(value: T): PathSet {
|
||||
if (value instanceof PathSet) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (isPathSet(value)) {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
|
||||
value.forEach((path: Array<HopObject>) => {
|
||||
bytes.push(Path.from(path).toBytes())
|
||||
bytes.push(Uint8Array.from([PATH_SEPARATOR_BYTE]))
|
||||
})
|
||||
|
||||
bytes[bytes.length - 1] = Uint8Array.from([PATHSET_END_BYTE])
|
||||
|
||||
return new PathSet(concat(bytes))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct PathSet from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a PathSet from a BinaryParser
|
||||
*
|
||||
* @param parser A BinaryParser to read PathSet from
|
||||
* @returns the PathSet read from parser
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): PathSet {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
|
||||
while (!parser.end()) {
|
||||
bytes.push(Path.fromParser(parser).toBytes())
|
||||
bytes.push(parser.read(1))
|
||||
|
||||
if (bytes[bytes.length - 1][0] == PATHSET_END_BYTE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return new PathSet(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of this PathSet
|
||||
*
|
||||
* @returns an Array of Array of HopObjects, representing this PathSet
|
||||
*/
|
||||
toJSON(): Array<Array<HopObject>> {
|
||||
const json: Array<Array<HopObject>> = []
|
||||
const pathParser = new BinaryParser(this.toString())
|
||||
|
||||
while (!pathParser.end()) {
|
||||
json.push(Path.fromParser(pathParser).toJSON())
|
||||
pathParser.skip(1)
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export { PathSet }
|
||||
122
packages/xahau-binary-codec/src/types/serialized-type.ts
Normal file
122
packages/xahau-binary-codec/src/types/serialized-type.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { BytesList } from '../serdes/binary-serializer'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { XrplDefinitionsBase } from '../enums'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
type JSON = string | number | boolean | null | undefined | JSON[] | JsonObject
|
||||
|
||||
type JsonObject = { [key: string]: JSON }
|
||||
|
||||
/**
|
||||
* The base class for all binary-codec types
|
||||
*/
|
||||
class SerializedType {
|
||||
protected readonly bytes: Uint8Array = new Uint8Array(0)
|
||||
|
||||
constructor(bytes?: Uint8Array) {
|
||||
this.bytes = bytes ?? new Uint8Array(0)
|
||||
}
|
||||
|
||||
static fromParser(parser: BinaryParser, hint?: number): SerializedType {
|
||||
throw new Error('fromParser not implemented')
|
||||
return this.fromParser(parser, hint)
|
||||
}
|
||||
|
||||
static from(value: SerializedType | JSON | bigint): SerializedType {
|
||||
throw new Error('from not implemented')
|
||||
return this.from(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the bytes representation of a SerializedType to a BytesList
|
||||
*
|
||||
* @param list The BytesList to write SerializedType bytes to
|
||||
*/
|
||||
toBytesSink(list: BytesList): void {
|
||||
list.put(this.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hex representation of a SerializedType's bytes
|
||||
*
|
||||
* @returns hex String of this.bytes
|
||||
*/
|
||||
toHex(): string {
|
||||
return bytesToHex(this.toBytes())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bytes representation of a SerializedType
|
||||
*
|
||||
* @returns A Uint8Array of the bytes
|
||||
*/
|
||||
toBytes(): Uint8Array {
|
||||
if (this.bytes) {
|
||||
return this.bytes
|
||||
}
|
||||
const bytes = new BytesList()
|
||||
this.toBytesSink(bytes)
|
||||
return bytes.toBytes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the JSON representation of a SerializedType
|
||||
*
|
||||
* @param _definitions xahaud definitions used to parse the values of transaction types and such.
|
||||
* Unused in default, but used in STObject, STArray
|
||||
* Can be customized for sidechains and amendments.
|
||||
* @returns any type, if not overloaded returns hexString representation of bytes
|
||||
*/
|
||||
toJSON(_definitions?: XrplDefinitionsBase): JSON {
|
||||
return this.toHex()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns hexString representation of this.bytes
|
||||
*/
|
||||
toString(): string {
|
||||
return this.toHex()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for SerializedTypes that are comparable.
|
||||
*
|
||||
* @template T - What types you want to allow comparisons between. You must specify all types. Primarily used to allow
|
||||
* comparisons between built-in types (like `string`) and SerializedType subclasses (like `Hash`).
|
||||
*
|
||||
* Ex. `class Hash extends Comparable<Hash | string>`
|
||||
*/
|
||||
class Comparable<T extends Object> extends SerializedType {
|
||||
lt(other: T): boolean {
|
||||
return this.compareTo(other) < 0
|
||||
}
|
||||
|
||||
eq(other: T): boolean {
|
||||
return this.compareTo(other) === 0
|
||||
}
|
||||
|
||||
gt(other: T): boolean {
|
||||
return this.compareTo(other) > 0
|
||||
}
|
||||
|
||||
gte(other: T): boolean {
|
||||
return this.compareTo(other) > -1
|
||||
}
|
||||
|
||||
lte(other: T): boolean {
|
||||
return this.compareTo(other) < 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload this method to define how two Comparable SerializedTypes are compared
|
||||
*
|
||||
* @param other The comparable object to compare this to
|
||||
* @returns A number denoting the relationship of this and other
|
||||
*/
|
||||
compareTo(other: T): number {
|
||||
throw new Error(`cannot compare ${this.toString()} and ${other.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
export { SerializedType, Comparable, JSON, JsonObject }
|
||||
113
packages/xahau-binary-codec/src/types/st-array.ts
Normal file
113
packages/xahau-binary-codec/src/types/st-array.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { DEFAULT_DEFINITIONS, XrplDefinitionsBase } from '../enums'
|
||||
import { SerializedType, JsonObject } from './serialized-type'
|
||||
import { STObject } from './st-object'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { concat } from '@xrplf/isomorphic/utils'
|
||||
|
||||
const ARRAY_END_MARKER = Uint8Array.from([0xf1])
|
||||
const ARRAY_END_MARKER_NAME = 'ArrayEndMarker'
|
||||
|
||||
const OBJECT_END_MARKER = Uint8Array.from([0xe1])
|
||||
|
||||
/**
|
||||
* TypeGuard for Array<JsonObject>
|
||||
*/
|
||||
function isObjects(args): args is Array<JsonObject> {
|
||||
return (
|
||||
Array.isArray(args) &&
|
||||
args.every(
|
||||
(arg) =>
|
||||
typeof arg === 'object' &&
|
||||
Object.keys(arg).length === 1 &&
|
||||
typeof Object.values(arg)[0] === 'object',
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing and deserializing Arrays of Objects
|
||||
*/
|
||||
class STArray extends SerializedType {
|
||||
/**
|
||||
* Construct an STArray from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to parse an STArray from
|
||||
* @returns An STArray Object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): STArray {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
|
||||
while (!parser.end()) {
|
||||
const field = parser.readField()
|
||||
if (field.name === ARRAY_END_MARKER_NAME) {
|
||||
break
|
||||
}
|
||||
|
||||
bytes.push(
|
||||
field.header,
|
||||
parser.readFieldValue(field).toBytes(),
|
||||
OBJECT_END_MARKER,
|
||||
)
|
||||
}
|
||||
|
||||
bytes.push(ARRAY_END_MARKER)
|
||||
return new STArray(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an STArray from an Array of JSON Objects
|
||||
*
|
||||
* @param value STArray or Array of Objects to parse into an STArray
|
||||
* @param definitions optional, types and values to use to encode/decode a transaction
|
||||
* @returns An STArray object
|
||||
*/
|
||||
static from<T extends STArray | Array<JsonObject>>(
|
||||
value: T,
|
||||
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
|
||||
): STArray {
|
||||
if (value instanceof STArray) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (isObjects(value)) {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
value.forEach((obj) => {
|
||||
bytes.push(STObject.from(obj, undefined, definitions).toBytes())
|
||||
})
|
||||
|
||||
bytes.push(ARRAY_END_MARKER)
|
||||
return new STArray(concat(bytes))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct STArray from value given')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the JSON representation of this.bytes
|
||||
*
|
||||
* @param definitions optional, types and values to use to encode/decode a transaction
|
||||
* @returns An Array of JSON objects
|
||||
*/
|
||||
toJSON(
|
||||
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
|
||||
): Array<JsonObject> {
|
||||
const result: Array<JsonObject> = []
|
||||
|
||||
const arrayParser = new BinaryParser(this.toString(), definitions)
|
||||
|
||||
while (!arrayParser.end()) {
|
||||
const field = arrayParser.readField()
|
||||
if (field.name === ARRAY_END_MARKER_NAME) {
|
||||
break
|
||||
}
|
||||
|
||||
const outer = {}
|
||||
outer[field.name] = STObject.fromParser(arrayParser).toJSON(definitions)
|
||||
result.push(outer)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export { STArray }
|
||||
192
packages/xahau-binary-codec/src/types/st-object.ts
Normal file
192
packages/xahau-binary-codec/src/types/st-object.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
DEFAULT_DEFINITIONS,
|
||||
FieldInstance,
|
||||
Bytes,
|
||||
XrplDefinitionsBase,
|
||||
} from '../enums'
|
||||
import { SerializedType, JsonObject } from './serialized-type'
|
||||
import { xAddressToClassicAddress, isValidXAddress } from 'xahau-address-codec'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { BinarySerializer, BytesList } from '../serdes/binary-serializer'
|
||||
|
||||
import { STArray } from './st-array'
|
||||
|
||||
const OBJECT_END_MARKER_BYTE = Uint8Array.from([0xe1])
|
||||
const OBJECT_END_MARKER = 'ObjectEndMarker'
|
||||
const ST_OBJECT = 'STObject'
|
||||
const DESTINATION = 'Destination'
|
||||
const ACCOUNT = 'Account'
|
||||
const SOURCE_TAG = 'SourceTag'
|
||||
const DEST_TAG = 'DestinationTag'
|
||||
|
||||
/**
|
||||
* Break down an X-Address into an account and a tag
|
||||
*
|
||||
* @param field Name of field
|
||||
* @param xAddress X-Address corresponding to the field
|
||||
*/
|
||||
function handleXAddress(field: string, xAddress: string): JsonObject {
|
||||
const decoded = xAddressToClassicAddress(xAddress)
|
||||
|
||||
let tagName
|
||||
if (field === DESTINATION) tagName = DEST_TAG
|
||||
else if (field === ACCOUNT) tagName = SOURCE_TAG
|
||||
else if (decoded.tag !== false)
|
||||
throw new Error(`${field} cannot have an associated tag`)
|
||||
|
||||
return decoded.tag !== false
|
||||
? { [field]: decoded.classicAddress, [tagName]: decoded.tag }
|
||||
: { [field]: decoded.classicAddress }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that two objects don't both have the same tag fields
|
||||
*
|
||||
* @param obj1 First object to check for tags
|
||||
* @param obj2 Second object to check for tags
|
||||
* @throws When both objects have SourceTag or DestinationTag
|
||||
*/
|
||||
function checkForDuplicateTags(obj1: JsonObject, obj2: JsonObject): void {
|
||||
if (!(obj1[SOURCE_TAG] === undefined || obj2[SOURCE_TAG] === undefined))
|
||||
throw new Error('Cannot have Account X-Address and SourceTag')
|
||||
if (!(obj1[DEST_TAG] === undefined || obj2[DEST_TAG] === undefined))
|
||||
throw new Error('Cannot have Destination X-Address and DestinationTag')
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for Serializing/Deserializing objects
|
||||
*/
|
||||
class STObject extends SerializedType {
|
||||
/**
|
||||
* Construct a STObject from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read STObject from
|
||||
* @returns A STObject object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): STObject {
|
||||
const list: BytesList = new BytesList()
|
||||
const bytes: BinarySerializer = new BinarySerializer(list)
|
||||
|
||||
while (!parser.end()) {
|
||||
const field = parser.readField()
|
||||
if (field.name === OBJECT_END_MARKER) {
|
||||
break
|
||||
}
|
||||
|
||||
const associatedValue = parser.readFieldValue(field)
|
||||
|
||||
bytes.writeFieldAndValue(field, associatedValue)
|
||||
if (field.type.name === ST_OBJECT) {
|
||||
bytes.put(OBJECT_END_MARKER_BYTE)
|
||||
}
|
||||
}
|
||||
|
||||
return new STObject(list.toBytes())
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a STObject from a JSON object
|
||||
*
|
||||
* @param value An object to include
|
||||
* @param filter optional, denote which field to include in serialized object
|
||||
* @param definitions optional, types and values to use to encode/decode a transaction
|
||||
* @returns a STObject object
|
||||
*/
|
||||
static from<T extends STObject | JsonObject>(
|
||||
value: T,
|
||||
filter?: (...any) => boolean,
|
||||
definitions: XrplDefinitionsBase = DEFAULT_DEFINITIONS,
|
||||
): STObject {
|
||||
if (value instanceof STObject) {
|
||||
return value
|
||||
}
|
||||
|
||||
const list: BytesList = new BytesList()
|
||||
const bytes: BinarySerializer = new BinarySerializer(list)
|
||||
|
||||
let isUnlModify = false
|
||||
|
||||
const xAddressDecoded = Object.entries(value).reduce((acc, [key, val]) => {
|
||||
let handled: JsonObject | undefined = undefined
|
||||
if (val && isValidXAddress(val.toString())) {
|
||||
handled = handleXAddress(key, val.toString())
|
||||
checkForDuplicateTags(handled, value)
|
||||
}
|
||||
return Object.assign(acc, handled ?? { [key]: val })
|
||||
}, {})
|
||||
|
||||
let sorted = Object.keys(xAddressDecoded)
|
||||
.map((f: string): FieldInstance => definitions.field[f] as FieldInstance)
|
||||
.filter(
|
||||
(f: FieldInstance): boolean =>
|
||||
f !== undefined &&
|
||||
xAddressDecoded[f.name] !== undefined &&
|
||||
f.isSerialized,
|
||||
)
|
||||
.sort((a, b) => {
|
||||
return a.ordinal - b.ordinal
|
||||
})
|
||||
|
||||
if (filter !== undefined) {
|
||||
sorted = sorted.filter(filter)
|
||||
}
|
||||
|
||||
sorted.forEach((field) => {
|
||||
const associatedValue =
|
||||
field.type.name === ST_OBJECT
|
||||
? this.from(xAddressDecoded[field.name], undefined, definitions)
|
||||
: field.type.name === 'STArray'
|
||||
? STArray.from(xAddressDecoded[field.name], definitions)
|
||||
: field.associatedType.from(xAddressDecoded[field.name])
|
||||
|
||||
if (associatedValue == undefined) {
|
||||
throw new TypeError(
|
||||
`Unable to interpret "${field.name}: ${
|
||||
xAddressDecoded[field.name]
|
||||
}".`,
|
||||
)
|
||||
}
|
||||
|
||||
if ((associatedValue as unknown as Bytes).name === 'UNLModify') {
|
||||
// triggered when the TransactionType field has a value of 'UNLModify'
|
||||
isUnlModify = true
|
||||
}
|
||||
// true when in the UNLModify pseudotransaction (after the transaction type has been processed) and working with the
|
||||
// Account field
|
||||
// The Account field must not be a part of the UNLModify pseudotransaction encoding, due to a bug in xahaud
|
||||
const isUnlModifyWorkaround = field.name == 'Account' && isUnlModify
|
||||
bytes.writeFieldAndValue(field, associatedValue, isUnlModifyWorkaround)
|
||||
if (field.type.name === ST_OBJECT) {
|
||||
bytes.put(OBJECT_END_MARKER_BYTE)
|
||||
}
|
||||
})
|
||||
|
||||
return new STObject(list.toBytes())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON interpretation of this.bytes
|
||||
* @param definitions xahaud definitions used to parse the values of transaction types and such.
|
||||
* Can be customized for sidechains and amendments.
|
||||
* @returns a JSON object
|
||||
*/
|
||||
toJSON(definitions?: XrplDefinitionsBase): JsonObject {
|
||||
const objectParser = new BinaryParser(this.toString(), definitions)
|
||||
const accumulator = {}
|
||||
|
||||
while (!objectParser.end()) {
|
||||
const field = objectParser.readField()
|
||||
if (field.name === OBJECT_END_MARKER) {
|
||||
break
|
||||
}
|
||||
|
||||
accumulator[field.name] = objectParser
|
||||
.readFieldValue(field)
|
||||
.toJSON(definitions)
|
||||
}
|
||||
|
||||
return accumulator
|
||||
}
|
||||
}
|
||||
|
||||
export { STObject }
|
||||
53
packages/xahau-binary-codec/src/types/uint-16.ts
Normal file
53
packages/xahau-binary-codec/src/types/uint-16.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { UInt } from './uint'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { readUInt16BE, writeUInt16BE } from '../utils'
|
||||
|
||||
/**
|
||||
* Derived UInt class for serializing/deserializing 16 bit UInt
|
||||
*/
|
||||
class UInt16 extends UInt {
|
||||
protected static readonly width: number = 16 / 8 // 2
|
||||
static readonly defaultUInt16: UInt16 = new UInt16(
|
||||
new Uint8Array(UInt16.width),
|
||||
)
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? UInt16.defaultUInt16.bytes)
|
||||
}
|
||||
|
||||
static fromParser(parser: BinaryParser): UInt {
|
||||
return new UInt16(parser.read(UInt16.width))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a UInt16 object from a number
|
||||
*
|
||||
* @param val UInt16 object or number
|
||||
*/
|
||||
static from<T extends UInt16 | number>(val: T): UInt16 {
|
||||
if (val instanceof UInt16) {
|
||||
return val
|
||||
}
|
||||
|
||||
if (typeof val === 'number') {
|
||||
UInt16.checkUintRange(val, 0, 0xffff)
|
||||
|
||||
const buf = new Uint8Array(UInt16.width)
|
||||
writeUInt16BE(buf, val, 0)
|
||||
return new UInt16(buf)
|
||||
}
|
||||
|
||||
throw new Error('Can not construct UInt16 with given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* get the value of a UInt16 object
|
||||
*
|
||||
* @returns the number represented by this.bytes
|
||||
*/
|
||||
valueOf(): number {
|
||||
return parseInt(readUInt16BE(this.bytes, 0))
|
||||
}
|
||||
}
|
||||
|
||||
export { UInt16 }
|
||||
59
packages/xahau-binary-codec/src/types/uint-32.ts
Normal file
59
packages/xahau-binary-codec/src/types/uint-32.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { UInt } from './uint'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { readUInt32BE, writeUInt32BE } from '../utils'
|
||||
|
||||
/**
|
||||
* Derived UInt class for serializing/deserializing 32 bit UInt
|
||||
*/
|
||||
class UInt32 extends UInt {
|
||||
protected static readonly width: number = 32 / 8 // 4
|
||||
static readonly defaultUInt32: UInt32 = new UInt32(
|
||||
new Uint8Array(UInt32.width),
|
||||
)
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? UInt32.defaultUInt32.bytes)
|
||||
}
|
||||
|
||||
static fromParser(parser: BinaryParser): UInt {
|
||||
return new UInt32(parser.read(UInt32.width))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a UInt32 object from a number
|
||||
*
|
||||
* @param val UInt32 object or number
|
||||
*/
|
||||
static from<T extends UInt32 | number | string>(val: T): UInt32 {
|
||||
if (val instanceof UInt32) {
|
||||
return val
|
||||
}
|
||||
|
||||
const buf = new Uint8Array(UInt32.width)
|
||||
|
||||
if (typeof val === 'string') {
|
||||
const num = Number.parseInt(val)
|
||||
writeUInt32BE(buf, num, 0)
|
||||
return new UInt32(buf)
|
||||
}
|
||||
|
||||
if (typeof val === 'number') {
|
||||
UInt32.checkUintRange(val, 0, 0xffffffff)
|
||||
writeUInt32BE(buf, val, 0)
|
||||
return new UInt32(buf)
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct UInt32 from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* get the value of a UInt32 object
|
||||
*
|
||||
* @returns the number represented by this.bytes
|
||||
*/
|
||||
valueOf(): number {
|
||||
return parseInt(readUInt32BE(this.bytes, 0), 10)
|
||||
}
|
||||
}
|
||||
|
||||
export { UInt32 }
|
||||
104
packages/xahau-binary-codec/src/types/uint-64.ts
Normal file
104
packages/xahau-binary-codec/src/types/uint-64.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { UInt } from './uint'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { bytesToHex, concat, hexToBytes } from '@xrplf/isomorphic/utils'
|
||||
import { readUInt32BE, writeUInt32BE } from '../utils'
|
||||
|
||||
const HEX_REGEX = /^[a-fA-F0-9]{1,16}$/
|
||||
const mask = BigInt(0x00000000ffffffff)
|
||||
|
||||
/**
|
||||
* Derived UInt class for serializing/deserializing 64 bit UInt
|
||||
*/
|
||||
class UInt64 extends UInt {
|
||||
protected static readonly width: number = 64 / 8 // 8
|
||||
static readonly defaultUInt64: UInt64 = new UInt64(
|
||||
new Uint8Array(UInt64.width),
|
||||
)
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? UInt64.defaultUInt64.bytes)
|
||||
}
|
||||
|
||||
static fromParser(parser: BinaryParser): UInt {
|
||||
return new UInt64(parser.read(UInt64.width))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a UInt64 object
|
||||
*
|
||||
* @param val A UInt64, hex-string, bigInt, or number
|
||||
* @returns A UInt64 object
|
||||
*/
|
||||
static from<T extends UInt64 | string | bigint | number>(val: T): UInt64 {
|
||||
if (val instanceof UInt64) {
|
||||
return val
|
||||
}
|
||||
|
||||
let buf = new Uint8Array(UInt64.width)
|
||||
|
||||
if (typeof val === 'number') {
|
||||
if (val < 0) {
|
||||
throw new Error('value must be an unsigned integer')
|
||||
}
|
||||
|
||||
const number = BigInt(val)
|
||||
|
||||
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
|
||||
writeUInt32BE(intBuf[0], Number(number >> BigInt(32)), 0)
|
||||
writeUInt32BE(intBuf[1], Number(number & BigInt(mask)), 0)
|
||||
|
||||
return new UInt64(concat(intBuf))
|
||||
}
|
||||
|
||||
if (typeof val === 'string') {
|
||||
if (!HEX_REGEX.test(val)) {
|
||||
throw new Error(`${val} is not a valid hex-string`)
|
||||
}
|
||||
|
||||
const strBuf = val.padStart(16, '0')
|
||||
buf = hexToBytes(strBuf)
|
||||
return new UInt64(buf)
|
||||
}
|
||||
|
||||
if (typeof val === 'bigint') {
|
||||
const intBuf = [new Uint8Array(4), new Uint8Array(4)]
|
||||
writeUInt32BE(intBuf[0], Number(Number(val >> BigInt(32))), 0)
|
||||
writeUInt32BE(intBuf[1], Number(val & BigInt(mask)), 0)
|
||||
|
||||
return new UInt64(concat(intBuf))
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct UInt64 from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* The JSON representation of a UInt64 object
|
||||
*
|
||||
* @returns a hex-string
|
||||
*/
|
||||
toJSON(): string {
|
||||
return bytesToHex(this.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the UInt64
|
||||
*
|
||||
* @returns the number represented buy this.bytes
|
||||
*/
|
||||
valueOf(): bigint {
|
||||
const msb = BigInt(readUInt32BE(this.bytes.slice(0, 4), 0))
|
||||
const lsb = BigInt(readUInt32BE(this.bytes.slice(4), 0))
|
||||
return (msb << BigInt(32)) | lsb
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bytes representation of the UInt64 object
|
||||
*
|
||||
* @returns 8 bytes representing the UInt64
|
||||
*/
|
||||
toBytes(): Uint8Array {
|
||||
return this.bytes
|
||||
}
|
||||
}
|
||||
|
||||
export { UInt64 }
|
||||
52
packages/xahau-binary-codec/src/types/uint-8.ts
Normal file
52
packages/xahau-binary-codec/src/types/uint-8.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { UInt } from './uint'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
import { writeUInt8 } from '../utils'
|
||||
|
||||
/**
|
||||
* Derived UInt class for serializing/deserializing 8 bit UInt
|
||||
*/
|
||||
class UInt8 extends UInt {
|
||||
protected static readonly width: number = 8 / 8 // 1
|
||||
static readonly defaultUInt8: UInt8 = new UInt8(new Uint8Array(UInt8.width))
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? UInt8.defaultUInt8.bytes)
|
||||
}
|
||||
|
||||
static fromParser(parser: BinaryParser): UInt {
|
||||
return new UInt8(parser.read(UInt8.width))
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a UInt8 object from a number
|
||||
*
|
||||
* @param val UInt8 object or number
|
||||
*/
|
||||
static from<T extends UInt8 | number>(val: T): UInt8 {
|
||||
if (val instanceof UInt8) {
|
||||
return val
|
||||
}
|
||||
|
||||
if (typeof val === 'number') {
|
||||
UInt8.checkUintRange(val, 0, 0xff)
|
||||
|
||||
const buf = new Uint8Array(UInt8.width)
|
||||
writeUInt8(buf, val, 0)
|
||||
return new UInt8(buf)
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct UInt8 from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* get the value of a UInt8 object
|
||||
*
|
||||
* @returns the number represented by this.bytes
|
||||
*/
|
||||
valueOf(): number {
|
||||
return parseInt(bytesToHex(this.bytes), 16)
|
||||
}
|
||||
}
|
||||
|
||||
export { UInt8 }
|
||||
60
packages/xahau-binary-codec/src/types/uint.ts
Normal file
60
packages/xahau-binary-codec/src/types/uint.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Comparable } from './serialized-type'
|
||||
|
||||
/**
|
||||
* Compare numbers and bigInts n1 and n2
|
||||
*
|
||||
* @param n1 First object to compare
|
||||
* @param n2 Second object to compare
|
||||
* @returns -1, 0, or 1, depending on how the two objects compare
|
||||
*/
|
||||
function compare(n1: number | bigint, n2: number | bigint): number {
|
||||
return n1 < n2 ? -1 : n1 == n2 ? 0 : 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for serializing and deserializing unsigned integers.
|
||||
*/
|
||||
abstract class UInt extends Comparable<UInt | number> {
|
||||
protected static width: number
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload of compareTo for Comparable
|
||||
*
|
||||
* @param other other UInt to compare this to
|
||||
* @returns -1, 0, or 1 depending on how the objects relate to each other
|
||||
*/
|
||||
compareTo(other: UInt | number): number {
|
||||
return compare(this.valueOf(), other.valueOf())
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UInt object to JSON
|
||||
*
|
||||
* @returns number or string represented by this.bytes
|
||||
*/
|
||||
toJSON(): number | string {
|
||||
const val = this.valueOf()
|
||||
return typeof val === 'number' ? val : val.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the UInt represented by this.bytes
|
||||
*
|
||||
* @returns the value
|
||||
*/
|
||||
abstract valueOf(): number | bigint
|
||||
|
||||
static checkUintRange(val: number, min: number, max: number): void {
|
||||
if (val < min || val > max) {
|
||||
throw new Error(
|
||||
`Invalid ${this.constructor.name}: ${val} must be >= ${min} and <= ${max}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UInt }
|
||||
79
packages/xahau-binary-codec/src/types/vector-256.ts
Normal file
79
packages/xahau-binary-codec/src/types/vector-256.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { SerializedType } from './serialized-type'
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
import { Hash256 } from './hash-256'
|
||||
import { BytesList } from '../serdes/binary-serializer'
|
||||
import { bytesToHex } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* TypeGuard for Array<string>
|
||||
*/
|
||||
function isStrings(arg): arg is Array<string> {
|
||||
return Array.isArray(arg) && (arg.length === 0 || typeof arg[0] === 'string')
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing and deserializing vectors of Hash256
|
||||
*/
|
||||
class Vector256 extends SerializedType {
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Vector256 from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to
|
||||
* @param hint length of the vector, in bytes, optional
|
||||
* @returns a Vector256 object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser, hint?: number): Vector256 {
|
||||
const bytesList = new BytesList()
|
||||
const bytes = hint ?? parser.size()
|
||||
const hashes = bytes / 32
|
||||
for (let i = 0; i < hashes; i++) {
|
||||
Hash256.fromParser(parser).toBytesSink(bytesList)
|
||||
}
|
||||
return new Vector256(bytesList.toBytes())
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Vector256 object from an array of hashes
|
||||
*
|
||||
* @param value A Vector256 object or array of hex-strings representing Hash256's
|
||||
* @returns a Vector256 object
|
||||
*/
|
||||
static from<T extends Vector256 | Array<string>>(value: T): Vector256 {
|
||||
if (value instanceof Vector256) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (isStrings(value)) {
|
||||
const bytesList = new BytesList()
|
||||
value.forEach((hash) => {
|
||||
Hash256.from(hash).toBytesSink(bytesList)
|
||||
})
|
||||
return new Vector256(bytesList.toBytes())
|
||||
}
|
||||
|
||||
throw new Error('Cannot construct Vector256 from given value')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an Array of hex-strings represented by this.bytes
|
||||
*
|
||||
* @returns An Array of strings representing the Hash256 objects
|
||||
*/
|
||||
toJSON(): Array<string> {
|
||||
if (this.bytes.byteLength % 32 !== 0) {
|
||||
throw new Error('Invalid bytes for Vector256')
|
||||
}
|
||||
|
||||
const result: Array<string> = []
|
||||
for (let i = 0; i < this.bytes.byteLength; i += 32) {
|
||||
result.push(bytesToHex(this.bytes.slice(i, i + 32)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export { Vector256 }
|
||||
128
packages/xahau-binary-codec/src/types/xchain-bridge.ts
Normal file
128
packages/xahau-binary-codec/src/types/xchain-bridge.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { BinaryParser } from '../serdes/binary-parser'
|
||||
|
||||
import { AccountID } from './account-id'
|
||||
import { JsonObject, SerializedType } from './serialized-type'
|
||||
import { Issue, IssueObject } from './issue'
|
||||
import { concat } from '@xrplf/isomorphic/utils'
|
||||
|
||||
/**
|
||||
* Interface for JSON objects that represent cross-chain bridges
|
||||
*/
|
||||
interface XChainBridgeObject extends JsonObject {
|
||||
LockingChainDoor: string
|
||||
LockingChainIssue: IssueObject | string
|
||||
IssuingChainDoor: string
|
||||
IssuingChainIssue: IssueObject | string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for XChainBridgeObject
|
||||
*/
|
||||
function isXChainBridgeObject(arg): arg is XChainBridgeObject {
|
||||
const keys = Object.keys(arg).sort()
|
||||
return (
|
||||
keys.length === 4 &&
|
||||
keys[0] === 'IssuingChainDoor' &&
|
||||
keys[1] === 'IssuingChainIssue' &&
|
||||
keys[2] === 'LockingChainDoor' &&
|
||||
keys[3] === 'LockingChainIssue'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for serializing/deserializing XChainBridges
|
||||
*/
|
||||
class XChainBridge extends SerializedType {
|
||||
static readonly ZERO_XCHAIN_BRIDGE: XChainBridge = new XChainBridge(
|
||||
concat([
|
||||
Uint8Array.from([0x14]),
|
||||
new Uint8Array(40),
|
||||
Uint8Array.from([0x14]),
|
||||
new Uint8Array(40),
|
||||
]),
|
||||
)
|
||||
|
||||
static readonly TYPE_ORDER: { name: string; type: typeof SerializedType }[] =
|
||||
[
|
||||
{ name: 'LockingChainDoor', type: AccountID },
|
||||
{ name: 'LockingChainIssue', type: Issue },
|
||||
{ name: 'IssuingChainDoor', type: AccountID },
|
||||
{ name: 'IssuingChainIssue', type: Issue },
|
||||
]
|
||||
|
||||
constructor(bytes: Uint8Array) {
|
||||
super(bytes ?? XChainBridge.ZERO_XCHAIN_BRIDGE.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a cross-chain bridge from a JSON
|
||||
*
|
||||
* @param value XChainBridge or JSON to parse into an XChainBridge
|
||||
* @returns An XChainBridge object
|
||||
*/
|
||||
static from<T extends XChainBridge | XChainBridgeObject>(
|
||||
value: T,
|
||||
): XChainBridge {
|
||||
if (value instanceof XChainBridge) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!isXChainBridgeObject(value)) {
|
||||
throw new Error('Invalid type to construct an XChainBridge')
|
||||
}
|
||||
|
||||
const bytes: Array<Uint8Array> = []
|
||||
this.TYPE_ORDER.forEach((item) => {
|
||||
const { name, type } = item
|
||||
if (type === AccountID) {
|
||||
bytes.push(Uint8Array.from([0x14]))
|
||||
}
|
||||
const object = type.from(value[name])
|
||||
bytes.push(object.toBytes())
|
||||
})
|
||||
return new XChainBridge(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an XChainBridge from a BinaryParser
|
||||
*
|
||||
* @param parser BinaryParser to read the XChainBridge from
|
||||
* @returns An XChainBridge object
|
||||
*/
|
||||
static fromParser(parser: BinaryParser): XChainBridge {
|
||||
const bytes: Array<Uint8Array> = []
|
||||
|
||||
this.TYPE_ORDER.forEach((item) => {
|
||||
const { type } = item
|
||||
if (type === AccountID) {
|
||||
parser.skip(1)
|
||||
bytes.push(Uint8Array.from([0x14]))
|
||||
}
|
||||
const object = type.fromParser(parser)
|
||||
bytes.push(object.toBytes())
|
||||
})
|
||||
|
||||
return new XChainBridge(concat(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of this XChainBridge
|
||||
*
|
||||
* @returns the JSON interpretation of this.bytes
|
||||
*/
|
||||
toJSON(): XChainBridgeObject {
|
||||
const parser = new BinaryParser(this.toString())
|
||||
const json = {}
|
||||
XChainBridge.TYPE_ORDER.forEach((item) => {
|
||||
const { name, type } = item
|
||||
if (type === AccountID) {
|
||||
parser.skip(1)
|
||||
}
|
||||
const object = type.fromParser(parser).toJSON()
|
||||
json[name] = object
|
||||
})
|
||||
return json as XChainBridgeObject
|
||||
}
|
||||
}
|
||||
|
||||
export { XChainBridge, XChainBridgeObject }
|
||||
152
packages/xahau-binary-codec/src/utils.ts
Normal file
152
packages/xahau-binary-codec/src/utils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// Even though this comes from NodeJS it is valid in the browser
|
||||
import TypedArray = NodeJS.TypedArray
|
||||
|
||||
/**
|
||||
* Writes value to array at the specified offset. The value must be a valid unsigned 8-bit integer.
|
||||
* @param array Uint8Array to be written to
|
||||
* @param value Number to be written to array.
|
||||
* @param offset plus the number of bytes written.
|
||||
*/
|
||||
export function writeUInt8(
|
||||
array: Uint8Array,
|
||||
value: number,
|
||||
offset: number,
|
||||
): void {
|
||||
value = Number(value)
|
||||
array[offset] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes value to array at the specified offset as big-endian. The value must be a valid unsigned 16-bit integer.
|
||||
* @param array Uint8Array to be written to
|
||||
* @param value Number to be written to array.
|
||||
* @param offset plus the number of bytes written.
|
||||
*/
|
||||
export function writeUInt16BE(
|
||||
array: Uint8Array,
|
||||
value: number,
|
||||
offset: number,
|
||||
): void {
|
||||
value = Number(value)
|
||||
|
||||
array[offset] = value >>> 8
|
||||
array[offset + 1] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes value to array at the specified offset as big-endian. The value must be a valid unsigned 32-bit integer.
|
||||
* @param array Uint8Array to be written to
|
||||
* @param value Number to be written to array.
|
||||
* @param offset plus the number of bytes written.
|
||||
*/
|
||||
export function writeUInt32BE(
|
||||
array: Uint8Array,
|
||||
value: number,
|
||||
offset: number,
|
||||
): void {
|
||||
array[offset] = (value >>> 24) & 0xff
|
||||
array[offset + 1] = (value >>> 16) & 0xff
|
||||
array[offset + 2] = (value >>> 8) & 0xff
|
||||
array[offset + 3] = value & 0xff
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned, big-endian 16-bit integer from the array at the specified offset.
|
||||
* @param array Uint8Array to read
|
||||
* @param offset Number of bytes to skip before starting to read. Must satisfy 0 <= offset <= buf.length - 2
|
||||
*/
|
||||
export function readUInt16BE(array: Uint8Array, offset: number): string {
|
||||
return new DataView(array.buffer).getUint16(offset, false).toString(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned, big-endian 16-bit integer from the array at the specified offset.
|
||||
* @param array Uint8Array to read
|
||||
* @param offset Number of bytes to skip before starting to read. Must satisfy 0 <= offset <= buf.length - 4
|
||||
*/
|
||||
export function readUInt32BE(array: Uint8Array, offset: number): string {
|
||||
return new DataView(array.buffer).getUint32(offset, false).toString(10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two Uint8Array or ArrayBuffers
|
||||
* @param a first array to compare
|
||||
* @param b second array to compare
|
||||
*/
|
||||
export function equal(
|
||||
a: Uint8Array | ArrayBuffer,
|
||||
b: Uint8Array | ArrayBuffer,
|
||||
): boolean {
|
||||
const aUInt = a instanceof ArrayBuffer ? new Uint8Array(a, 0) : a
|
||||
const bUInt = b instanceof ArrayBuffer ? new Uint8Array(b, 0) : b
|
||||
if (aUInt.byteLength != bUInt.byteLength) return false
|
||||
if (aligned32(aUInt) && aligned32(bUInt)) return compare32(aUInt, bUInt) === 0
|
||||
if (aligned16(aUInt) && aligned16(bUInt)) return compare16(aUInt, bUInt) === 0
|
||||
return compare8(aUInt, bUInt) === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two 8 bit aligned arrays
|
||||
* @param a first array to compare
|
||||
* @param b second array to compare
|
||||
*/
|
||||
function compare8(a, b) {
|
||||
const ua = new Uint8Array(a.buffer, a.byteOffset, a.byteLength)
|
||||
const ub = new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
|
||||
return compare(ua, ub)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two 16 bit aligned arrays
|
||||
* @param a first array to compare
|
||||
* @param b second array to compare
|
||||
*/
|
||||
function compare16(a: Uint8Array, b: Uint8Array) {
|
||||
const ua = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2)
|
||||
const ub = new Uint16Array(b.buffer, b.byteOffset, b.byteLength / 2)
|
||||
return compare(ua, ub)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two 32 bit aligned arrays
|
||||
* @param a first array to compare
|
||||
* @param b second array to compare
|
||||
*/
|
||||
function compare32(a: Uint8Array, b: Uint8Array) {
|
||||
const ua = new Uint32Array(a.buffer, a.byteOffset, a.byteLength / 4)
|
||||
const ub = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / 4)
|
||||
return compare(ua, ub)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two TypedArrays
|
||||
* @param a first array to compare
|
||||
* @param b second array to compare
|
||||
*/
|
||||
export function compare(a: TypedArray, b: TypedArray): 1 | -1 | 0 {
|
||||
if (a.byteLength !== b.byteLength) {
|
||||
throw new Error('Cannot compare arrays of different length')
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length - 1; i += 1) {
|
||||
if (a[i] > b[i]) return 1
|
||||
if (a[i] < b[i]) return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if TypedArray is 16 bit aligned
|
||||
* @param array The array to check
|
||||
*/
|
||||
function aligned16(array: TypedArray) {
|
||||
return array.byteOffset % 2 === 0 && array.byteLength % 2 === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if TypedArray is 32 bit aligned
|
||||
* @param array The array to check
|
||||
*/
|
||||
function aligned32(array: TypedArray) {
|
||||
return array.byteOffset % 4 === 0 && array.byteLength % 4 === 0
|
||||
}
|
||||
Reference in New Issue
Block a user