diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 317e1feb71..4b127b6b70 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -48,7 +48,11 @@ "Child": -2, "Nickname": 110, "Contract": 99, - "GeneratorMap": 103 + "GeneratorMap": 103, + "Hook": 72, + "HookState": 118, + "HookDefinition": 68, + "EmittedTxn": 69 }, "FIELDS": [ [ @@ -321,6 +325,16 @@ "type": "UInt16" } ], + [ + "NetworkID", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "Flags", { @@ -761,6 +775,16 @@ "type": "UInt32" } ], + [ + "LockCount", + { + "nth": 47, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -891,16 +915,6 @@ "type": "UInt64" } ], - [ - "HookOn", - { - "nth": 16, - "isVLEncoded": false, - "isSerialized": true, - "isSigningField": true, - "type": "UInt64" - } - ], [ "HookInstructionCount", { @@ -1151,6 +1165,16 @@ "type": "Hash256" } ], + [ + "HookOn", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "Digest", { @@ -1281,6 +1305,26 @@ "type": "Hash256" } ], + [ + "OfferID", + { + "nth": 34, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], + [ + "EscrowID", + { + "nth": 35, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "Amount", { @@ -1421,6 +1465,26 @@ "type": "Amount" } ], + [ + "HookCallbackFee", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LockedBalance", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "PublicKey", { @@ -1661,6 +1725,16 @@ "type": "Blob" } ], + [ + "Blob", + { + "nth": 26, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Blob" + } + ], [ "Account", { @@ -1801,6 +1875,16 @@ "type": "Vector256" } ], + [ + "HookNamespaces", + { + "nth": 5, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "Vector256" + } + ], [ "Paths", { @@ -2176,6 +2260,10 @@ "telCAN_NOT_QUEUE_BLOCKED": -389, "telCAN_NOT_QUEUE_FEE": -388, "telCAN_NOT_QUEUE_FULL": -387, + "telWRONG_NETWORK": -386, + "telREQUIRES_NETWORK_ID": -385, + "telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384, + "telNON_LOCAL_EMITTED_TXN": -383, "temMALFORMED": -299, "temBAD_AMOUNT": -298, @@ -2215,6 +2303,16 @@ "temUNKNOWN": -264, "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, + "temAMM_BAD_TOKENS": -261, + "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, + "temXCHAIN_BAD_PROOF": -259, + "temXCHAIN_BRIDGE_BAD_ISSUES": -258, + "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, + "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, + "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, + "temXCHAIN_TOO_MANY_ATTESTATIONS": -254, + "temHOOK_DATA_TOO_LARGE": -253, + "temHOOK_REJECTED": -252, "tefFAILURE": -199, "tefALREADY": -198, @@ -2250,6 +2348,8 @@ "terNO_RIPPLE": -90, "terQUEUED": -89, "terPRE_TICKET": -88, + "terNO_AMM": -87, + "terNO_HOOK": -86, "tesSUCCESS": 0, @@ -2291,6 +2391,7 @@ "tecKILLED": 150, "tecHAS_OBLIGATIONS": 151, "tecTOO_SOON": 152, + "tecHOOK_REJECTED": 153, "tecMAX_SEQUENCE_REACHED": 154, "tecNO_SUITABLE_NFTOKEN_PAGE": 155, "tecNFTOKEN_BUY_SELL_MISMATCH": 156, @@ -2298,7 +2399,32 @@ "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, "tecINSUFFICIENT_FUNDS": 159, "tecOBJECT_NOT_FOUND": 160, - "tecINSUFFICIENT_PAYMENT": 161 + "tecINSUFFICIENT_PAYMENT": 161, + "tecAMM_UNFUNDED": 162, + "tecAMM_BALANCE": 163, + "tecAMM_FAILED_DEPOSIT": 164, + "tecAMM_FAILED_WITHDRAW": 165, + "tecAMM_INVALID_TOKENS": 166, + "tecAMM_FAILED_BID": 167, + "tecAMM_FAILED_VOTE": 168, + "tecREQUIRES_FLAG": 169, + "tecPRECISION_LOSS": 170, + "tecBAD_XCHAIN_TRANSFER_ISSUE": 171, + "tecXCHAIN_NO_CLAIM_ID": 172, + "tecXCHAIN_BAD_CLAIM_ID": 173, + "tecXCHAIN_CLAIM_NO_QUORUM": 174, + "tecXCHAIN_PROOF_UNKNOWN_KEY": 175, + "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 176, + "tecXCHAIN_WRONG_CHAIN": 177, + "tecXCHAIN_REWARD_MISMATCH": 178, + "tecXCHAIN_NO_SIGNERS_LIST": 179, + "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 180, + "tecXCHAIN_INSUFF_CREATE_AMOUNT": 181, + "tecXCHAIN_ACCOUNT_CREATE_PAST": 182, + "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 183, + "tecXCHAIN_PAYMENT_FAILED": 184, + "tecXCHAIN_SELF_COMMIT": 185, + "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 186 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2330,8 +2456,10 @@ "NFTokenCreateOffer": 27, "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, + "Invoke": 99, "EnableAmendment": 100, "SetFee": 101, - "UNLModify": 102 + "UNLModify": 102, + "EmitFailure": 103 } } diff --git a/packages/ripple-binary-codec/src/enums/index.ts b/packages/ripple-binary-codec/src/enums/index.ts index 965c0a9883..5ee0553b45 100644 --- a/packages/ripple-binary-codec/src/enums/index.ts +++ b/packages/ripple-binary-codec/src/enums/index.ts @@ -19,6 +19,7 @@ const Field = DEFAULT_DEFINITIONS.field * @brief: All valid transaction types */ const TRANSACTION_TYPES = DEFAULT_DEFINITIONS.transactionNames +const TRANSACTION_TYPE_MAP = DEFAULT_DEFINITIONS.transactionMap export { Bytes, @@ -31,4 +32,5 @@ export { TransactionResult, TransactionType, TRANSACTION_TYPES, + TRANSACTION_TYPE_MAP, } diff --git a/packages/ripple-binary-codec/src/enums/xrpl-definitions-base.ts b/packages/ripple-binary-codec/src/enums/xrpl-definitions-base.ts index 1a61a31467..837236d022 100644 --- a/packages/ripple-binary-codec/src/enums/xrpl-definitions-base.ts +++ b/packages/ripple-binary-codec/src/enums/xrpl-definitions-base.ts @@ -33,6 +33,8 @@ class XrplDefinitionsBase { transactionType: BytesLookup // Valid transaction names transactionNames: string[] + // Valid transaction names + transactionMap: Record // Maps serializable types to their TypeScript class implementation dataTypes: Record @@ -68,10 +70,20 @@ class XrplDefinitionsBase { enums.FIELDS as Array<[string, FieldInfo]>, enums.TYPES, ) + this.transactionNames = Object.entries(enums.TRANSACTION_TYPES) .filter(([_key, value]) => value >= 0) .map(([key, _value]) => key) + const ignoreList = ['EnableAmendment', 'SetFee', 'UNLModify', 'EmitFailure'] + this.transactionMap = Object.assign( + {}, + ...Object.entries(enums.TRANSACTION_TYPES) + + .filter(([_key, _value]) => _value >= 0 || ignoreList.includes(_key)) + .map(([key, value]) => ({ [key]: value })), + ) + this.dataTypes = {} // Filled in via associateTypes this.associateTypes(types) } diff --git a/packages/ripple-binary-codec/src/index.ts b/packages/ripple-binary-codec/src/index.ts index 6d7852d240..4afcf8e7e7 100644 --- a/packages/ripple-binary-codec/src/index.ts +++ b/packages/ripple-binary-codec/src/index.ts @@ -6,6 +6,7 @@ import { JsonObject } from './types/serialized-type' import { XrplDefinitionsBase, TRANSACTION_TYPES, + TRANSACTION_TYPE_MAP, DEFAULT_DEFINITIONS, } from './enums' import { XrplDefinitions } from './enums/xrpl-definitions' @@ -134,6 +135,7 @@ export { decodeQuality, decodeLedgerData, TRANSACTION_TYPES, + TRANSACTION_TYPE_MAP, XrplDefinitions, XrplDefinitionsBase, DEFAULT_DEFINITIONS, diff --git a/packages/xrpl/src/models/common/index.ts b/packages/xrpl/src/models/common/index.ts index 2eeb7b17bf..e6020490dc 100644 --- a/packages/xrpl/src/models/common/index.ts +++ b/packages/xrpl/src/models/common/index.ts @@ -51,6 +51,83 @@ interface PathStep { export type Path = PathStep[] +/** + * The object that describes the grant in HookGrants. + */ +export interface HookGrant { + /** + * The object that describes the grant in HookGrants. + */ + HookGrant: { + /** + * The hook hash of the grant. + */ + HookHash: string + /** + * The account authorized on the grant. + */ + Authorize?: string + } +} + +/** + * The object that describes the parameter in HookParameters. + */ +export interface HookParameter { + /** + * The object that describes the parameter in HookParameters. + */ + HookParameter: { + /** + * The name of the parameter. + */ + HookParameterName: string + /** + * The value of the parameter. + */ + HookParameterValue: string + } +} + +/** + * The object that describes the hook in Hooks. + */ +export interface Hook { + /** + * The object that describes the hook in Hooks. + */ + Hook: { + /** + * The code that is executed when the hook is triggered. + */ + CreateCode: string + /** + * The flags that are set on the hook. + */ + Flags: number + /** + * The transactions that triggers the hook. Represented as a 256Hash + */ + HookOn?: string + /** + * The namespace of the hook. + */ + HookNamespace?: string + /** + * The API version of the hook. + */ + HookApiVersion?: number + /** + * The parameters of the hook. + */ + HookParameters?: HookParameter[] + /** + * The grants of the hook. + */ + HookGrants?: HookGrant[] + } +} + /** * The object that describes the signer in SignerEntries. */ diff --git a/packages/xrpl/src/models/ledger/EmittedTxn.ts b/packages/xrpl/src/models/ledger/EmittedTxn.ts new file mode 100644 index 0000000000..e2a690aa09 --- /dev/null +++ b/packages/xrpl/src/models/ledger/EmittedTxn.ts @@ -0,0 +1,20 @@ +import { Transaction } from '../transactions' + +import BaseLedgerEntry from './BaseLedgerEntry' + +/** + * The EmittedTxn object type contains the + * + * @category Ledger Entries + */ +export default interface EmittedTxn extends BaseLedgerEntry { + LedgerEntryType: 'EmittedTxn' + + EmittedTxn: Transaction + + /** + * A hint indicating which page of the sender's owner directory links to this + * object, in case the directory consists of multiple pages. + */ + OwnerNode: string +} diff --git a/packages/xrpl/src/models/ledger/Hook.ts b/packages/xrpl/src/models/ledger/Hook.ts new file mode 100644 index 0000000000..aeb31ebd1c --- /dev/null +++ b/packages/xrpl/src/models/ledger/Hook.ts @@ -0,0 +1,27 @@ +import { Hook as WHook } from '../common' + +import BaseLedgerEntry from './BaseLedgerEntry' + +/** + * The Hook object type contains the + * + * @category Ledger Entries + */ +export default interface Hook extends BaseLedgerEntry { + LedgerEntryType: 'Hook' + + /** The identifying (classic) address of this account. */ + Account: string + + /** + * A hint indicating which page of the sender's owner directory links to this + * object, in case the directory consists of multiple pages. + */ + OwnerNode: string + + PreviousTxnID: string + + PreviousTxnLgrSeq: number + + Hooks: WHook[] +} diff --git a/packages/xrpl/src/models/ledger/HookDefinition.ts b/packages/xrpl/src/models/ledger/HookDefinition.ts new file mode 100644 index 0000000000..547edbcc96 --- /dev/null +++ b/packages/xrpl/src/models/ledger/HookDefinition.ts @@ -0,0 +1,67 @@ +import { HookParameter } from '../common' + +import BaseLedgerEntry from './BaseLedgerEntry' + +/** + * The HookDefintion object type contains the + * + * @category Ledger Entries + */ +export default interface HookDefintion extends BaseLedgerEntry { + LedgerEntryType: 'HookDefintion' + + /** + * The flags that are set on the hook. + */ + Flags: number + + /** + * This field contains a string that is used to uniquely identify the hook. + */ + HookHash: string + + /** + * The transactions that triggers the hook. Represented as a 256Hash + */ + HookOn?: string + + /** + * The namespace of the hook. + */ + HookNamespace?: string + + /** + * The API version of the hook. + */ + HookApiVersion?: string + + /** + * The parameters of the hook. + */ + HookParameters?: HookParameter[] + + /** + * The code that is executed when the hook is triggered. + */ + CreateCode?: string + + /** + * This is an optional field that contains the transaction ID of the hook set. + */ + HookSetTxnID?: string + + /** + * This is an optional field that contains the number of references to this hook. + */ + ReferenceCount?: number + + /** + * This is an optional field that contains the fee associated with the hook. + */ + Fee?: string + + /** + * This is an optional field that contains the callback fee associated with the hook. + */ + HookCallbackFee?: number +} diff --git a/packages/xrpl/src/models/ledger/HookState.ts b/packages/xrpl/src/models/ledger/HookState.ts new file mode 100644 index 0000000000..f236afecf1 --- /dev/null +++ b/packages/xrpl/src/models/ledger/HookState.ts @@ -0,0 +1,29 @@ +import BaseLedgerEntry from './BaseLedgerEntry' + +/** + * The HookState object type contains the + * + * @category Ledger Entries + */ +export default interface HookState extends BaseLedgerEntry { + LedgerEntryType: 'HookState' + + /** + * A hint indicating which page of the sender's owner directory links to this + * object, in case the directory consists of multiple pages. + */ + OwnerNode: string + + /** + * The HookStateKey property contains the key associated with this hook state, + * and the HookStateData property contains the data associated with this hook state. + */ + HookStateKey: string + + /** + * The `HookStateData` property contains the data associated with this hook state. + * It is typically a string containing the data associated with this hook state, + * such as an identifier or other information. + */ + HookStateData: string +} diff --git a/packages/xrpl/src/models/ledger/LedgerEntry.ts b/packages/xrpl/src/models/ledger/LedgerEntry.ts index 1302c32e7d..33238ea823 100644 --- a/packages/xrpl/src/models/ledger/LedgerEntry.ts +++ b/packages/xrpl/src/models/ledger/LedgerEntry.ts @@ -3,8 +3,12 @@ import Amendments from './Amendments' import Check from './Check' import DepositPreauth from './DepositPreauth' import DirectoryNode from './DirectoryNode' +import EmittedTxn from './EmittedTxn' import Escrow from './Escrow' import FeeSettings from './FeeSettings' +import Hook from './Hook' +import HookDefinition from './HookDefinition' +import HookState from './HookState' import LedgerHashes from './LedgerHashes' import NegativeUNL from './NegativeUNL' import Offer from './Offer' @@ -19,8 +23,12 @@ type LedgerEntry = | Check | DepositPreauth | DirectoryNode + | EmittedTxn | Escrow | FeeSettings + | Hook + | HookDefinition + | HookState | LedgerHashes | NegativeUNL | Offer diff --git a/packages/xrpl/src/models/ledger/index.ts b/packages/xrpl/src/models/ledger/index.ts index fbb31d402c..efac2c12a6 100644 --- a/packages/xrpl/src/models/ledger/index.ts +++ b/packages/xrpl/src/models/ledger/index.ts @@ -6,8 +6,12 @@ import Amendments from './Amendments' import Check from './Check' import DepositPreauth from './DepositPreauth' import DirectoryNode from './DirectoryNode' +import EmittedTxn from './EmittedTxn' import Escrow from './Escrow' import FeeSettings from './FeeSettings' +import Hook from './Hook' +import HookDefinition from './HookDefinition' +import HookState from './HookState' import Ledger from './Ledger' import LedgerEntry from './LedgerEntry' import LedgerHashes from './LedgerHashes' @@ -26,8 +30,12 @@ export { Check, DepositPreauth, DirectoryNode, + EmittedTxn, Escrow, FeeSettings, + Hook, + HookDefinition, + HookState, Ledger, LedgerEntry, LedgerHashes, diff --git a/packages/xrpl/src/models/methods/ledgerEntry.ts b/packages/xrpl/src/models/methods/ledgerEntry.ts index f499aab84c..05e836835a 100644 --- a/packages/xrpl/src/models/methods/ledgerEntry.ts +++ b/packages/xrpl/src/models/methods/ledgerEntry.ts @@ -137,6 +137,40 @@ export interface LedgerEntryRequest extends BaseRequest { ticket_sequence: number } | string + /** + * The object ID of a transaction emitted by the ledger entry. + */ + emitted_txn?: string + + /** + * The hash of the Hook object to retrieve. + */ + hook_definition?: string + + /** + * The Hook object to retrieve. If a string, must be the object ID of the Hook. + * If an object, requires `account` sub-field. + */ + hook?: + | { + /** The account of the Hook object. */ + account: string + } + | string + + /** + * Object specifying the HookState object to retrieve. Requires the sub-fields + * `account`, `key`, and `namespace_id` to uniquely specify the HookState entry + * to retrieve. + */ + hook_state?: { + /** The account of the Hook object. */ + account: string + /** The key of the state. */ + key: string + /** The namespace of the state. */ + namespace_id: string + } } /** diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index 499444243c..2776250950 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -14,6 +14,7 @@ export { DepositPreauth } from './depositPreauth' export { EscrowCancel } from './escrowCancel' export { EscrowCreate } from './escrowCreate' export { EscrowFinish } from './escrowFinish' +export { Invoke } from './invoke' export { NFTokenAcceptOffer } from './NFTokenAcceptOffer' export { NFTokenBurn } from './NFTokenBurn' export { NFTokenCancelOffer } from './NFTokenCancelOffer' @@ -42,6 +43,7 @@ export { export { PaymentChannelCreate } from './paymentChannelCreate' export { PaymentChannelFund } from './paymentChannelFund' export { SetRegularKey } from './setRegularKey' +export { SetHook } from './setHook' export { SignerListSet } from './signerListSet' export { TicketCreate } from './ticketCreate' export { TrustSetFlagsInterface, TrustSetFlags, TrustSet } from './trustSet' diff --git a/packages/xrpl/src/models/transactions/invoke.ts b/packages/xrpl/src/models/transactions/invoke.ts new file mode 100644 index 0000000000..a7824adc00 --- /dev/null +++ b/packages/xrpl/src/models/transactions/invoke.ts @@ -0,0 +1,32 @@ +import { ValidationError } from '../../errors' + +import { BaseTransaction, validateBaseTransaction } from './common' + +/** + * + * + * @category Transaction Models + */ +export interface Invoke extends BaseTransaction { + TransactionType: 'Invoke' + /** + * If present, invokes the Hook on the Destination account. + */ + Destination?: string +} + +/** + * Verify the form and type of an Invoke at runtime. + * + * @param tx - An Invoke Transaction. + * @throws When the Invoke is Malformed. + */ +export function validateInvoke(tx: Record): void { + validateBaseTransaction(tx) + + if (tx.Account === tx.Destination) { + throw new ValidationError( + 'Invoke: Destination and Account must not be equal', + ) + } +} diff --git a/packages/xrpl/src/models/transactions/setHook.ts b/packages/xrpl/src/models/transactions/setHook.ts new file mode 100644 index 0000000000..e7f334ba9a --- /dev/null +++ b/packages/xrpl/src/models/transactions/setHook.ts @@ -0,0 +1,56 @@ +import { ValidationError } from '../../errors' +import { Hook } from '../common' + +import { BaseTransaction, validateBaseTransaction } from './common' + +/** + * + * + * @category Transaction Models + */ +export interface SetHook extends BaseTransaction { + TransactionType: 'SetHook' + /** + * + */ + Hooks: Hook[] +} + +const MAX_HOOKS = 4 +const HEX_REGEX = /^[0-9A-Fa-f]{64}$/u + +/** + * Verify the form and type of an SetHook at runtime. + * + * @param tx - An SetHook Transaction. + * @throws When the SetHook is Malformed. + */ +export function validateSetHook(tx: Record): void { + validateBaseTransaction(tx) + + if (!Array.isArray(tx.Hooks)) { + throw new ValidationError('SetHook: invalid Hooks') + } + + if (tx.Hooks.length > MAX_HOOKS) { + throw new ValidationError( + `SetHook: maximum of ${MAX_HOOKS} hooks allowed in Hooks`, + ) + } + + for (const hook of tx.Hooks) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Should be a Hook + const hookObject = hook as Hook + const { HookOn, HookNamespace } = hookObject.Hook + if (HookOn !== undefined && !HEX_REGEX.test(HookOn)) { + throw new ValidationError( + `SetHook: HookOn in Hook must be a 256-bit (32-byte) hexadecimal value`, + ) + } + if (HookNamespace !== undefined && !HEX_REGEX.test(HookNamespace)) { + throw new ValidationError( + `SetHook: HookNamespace in Hook must be a 256-bit (32-byte) hexadecimal value`, + ) + } + } +} diff --git a/packages/xrpl/src/models/transactions/transaction.ts b/packages/xrpl/src/models/transactions/transaction.ts index dfe0ffbbff..9ec3beed2c 100644 --- a/packages/xrpl/src/models/transactions/transaction.ts +++ b/packages/xrpl/src/models/transactions/transaction.ts @@ -43,6 +43,7 @@ import { PaymentChannelFund, validatePaymentChannelFund, } from './paymentChannelFund' +import { SetHook, validateSetHook } from './setHook' import { SetRegularKey, validateSetRegularKey } from './setRegularKey' import { SignerListSet, validateSignerListSet } from './signerListSet' import { TicketCreate, validateTicketCreate } from './ticketCreate' @@ -72,6 +73,7 @@ export type Transaction = | PaymentChannelClaim | PaymentChannelCreate | PaymentChannelFund + | SetHook | SetRegularKey | SignerListSet | TicketCreate @@ -188,6 +190,10 @@ export function validate(transaction: Record): void { validateSetRegularKey(tx) break + case 'SetHook': + validateSetHook(tx) + break + case 'SignerListSet': validateSignerListSet(tx) break diff --git a/packages/xrpl/src/sugar/autofill.ts b/packages/xrpl/src/sugar/autofill.ts index 05d50a961f..79718bdf1e 100644 --- a/packages/xrpl/src/sugar/autofill.ts +++ b/packages/xrpl/src/sugar/autofill.ts @@ -8,7 +8,7 @@ import { Transaction } from '../models/transactions' import { setTransactionFlagsToNumber } from '../models/utils/flags' import { xrpToDrops } from '../utils' -import getFeeXrp from './getFeeXrp' +import { getFeeXrp } from './getFeeXrp' // Expire unconfirmed transactions after 20 ledger versions, approximately 1 minute, by default const LEDGER_OFFSET = 20 diff --git a/packages/xrpl/src/sugar/getFeeXrp.ts b/packages/xrpl/src/sugar/getFeeXrp.ts index 0285d821ba..72d4ea6813 100644 --- a/packages/xrpl/src/sugar/getFeeXrp.ts +++ b/packages/xrpl/src/sugar/getFeeXrp.ts @@ -14,7 +14,7 @@ const BASE_10 = 10 * @param cushion - The fee cushion to use. * @returns The transaction fee. */ -export default async function getFeeXrp( +export async function getFeeXrp( client: Client, cushion?: number, ): Promise { @@ -43,3 +43,22 @@ export default async function getFeeXrp( // Round fee to 6 decimal places return new BigNumber(fee.toFixed(NUM_DECIMAL_PLACES)).toString(BASE_10) } + +/** + * Calculates the estimated transaction fee. + * Note: This is a public API that can be called directly. + * + * @param client - The Client used to connect to the ledger. + * @param txBlob - The encoded transaction to estimate the fee for. + * @returns The transaction fee. + */ +export async function getFeeEstimateXrp( + client: Client, + txBlob: string, +): Promise { + const response = await client.request({ + command: 'fee', + tx_blob: txBlob, + }) + return response.result.drops.base_fee +} diff --git a/packages/xrpl/src/sugar/index.ts b/packages/xrpl/src/sugar/index.ts index 991c1533b8..c132bf7fa8 100644 --- a/packages/xrpl/src/sugar/index.ts +++ b/packages/xrpl/src/sugar/index.ts @@ -5,6 +5,7 @@ export { getBalances, getXrpBalance } from './balances' export { default as getLedgerIndex } from './getLedgerIndex' export { default as getOrderbook } from './getOrderbook' +export { getFeeXrp, getFeeEstimateXrp } from './getFeeXrp' export * from './submit' diff --git a/packages/xrpl/src/utils/hooks.ts b/packages/xrpl/src/utils/hooks.ts new file mode 100644 index 0000000000..5066e85625 --- /dev/null +++ b/packages/xrpl/src/utils/hooks.ts @@ -0,0 +1,114 @@ +/** + * @module tts + * @description + * This module contains the transaction types and the function to calculate the hook on + */ + +// eslint-disable-next-line @typescript-eslint/no-require-imports -- Required +import createHash = require('create-hash') +import { TRANSACTION_TYPES, TRANSACTION_TYPE_MAP } from 'ripple-binary-codec' + +import { XrplError } from '../errors' +import { HookParameter } from '../models/common' + +/** + * @constant tts + * @description + * Transaction types + */ + +/** + * @typedef TTS + * @description + * Transaction types + */ +export type TTS = typeof TRANSACTION_TYPE_MAP + +/** + * Calculate the hook on + * + * @param arr - array of transaction types + * @returns the hook on + */ +export function calculateHookOn(arr: Array): string { + let hash = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff' + arr.forEach((nth) => { + if (typeof nth !== 'string') { + throw new XrplError(`HookOn transaction type must be string`) + } + if (!TRANSACTION_TYPES.includes(String(nth))) { + throw new XrplError( + `invalid transaction type '${String(nth)}' in HookOn array`, + ) + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Required + const tts: Record = TRANSACTION_TYPE_MAP + let value = BigInt(hash) + // eslint-disable-next-line no-bitwise -- Required + value ^= BigInt(1) << BigInt(tts[nth]) + // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required + hash = `0x${value.toString(16)}` + }) + hash = hash.replace('0x', '') + // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required + hash = hash.padStart(64, '0') + return hash.toUpperCase() +} + +/** + * Calculate the sha256 of a string + * + * @param string - the string to calculate the sha256 + * @returns the sha256 + */ +export async function sha256(string: string): Promise { + const hash = createHash('sha256') + hash.update(string) + const hashBuffer = hash.digest() + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const hashHex = hashArray + // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- Required + .map((bytes) => bytes.toString(16).padStart(2, '0')) + .join('') + return hashHex +} + +/** + * Calculate the hex of a namespace + * + * @param namespace - the namespace to calculate the hex + * @returns the hex + */ +export async function hexNamespace(namespace: string): Promise { + return (await sha256(namespace)).toUpperCase() +} + +/** + * Calculate the hex of the hook parameters + * + * @param data - the hook parameters + * @returns the hex of the hook parameters + */ +export function hexHookParameters(data: HookParameter[]): HookParameter[] { + const hookParameters: HookParameter[] = [] + for (const parameter of data) { + hookParameters.push({ + HookParameter: { + HookParameterName: Buffer.from( + parameter.HookParameter.HookParameterName, + 'utf8', + ) + .toString('hex') + .toUpperCase(), + HookParameterValue: Buffer.from( + parameter.HookParameter.HookParameterValue, + 'utf8', + ) + .toString('hex') + .toUpperCase(), + }, + }) + } + return hookParameters +} diff --git a/packages/xrpl/src/utils/index.ts b/packages/xrpl/src/utils/index.ts index 299b27a754..452bf38c11 100644 --- a/packages/xrpl/src/utils/index.ts +++ b/packages/xrpl/src/utils/index.ts @@ -40,6 +40,7 @@ import { hashEscrow, hashPaymentChannel, } from './hashes' +import { calculateHookOn, hexNamespace, hexHookParameters, TTS } from './hooks' import parseNFTokenID from './parseNFTokenID' import { percentToTransferRate, @@ -222,4 +223,8 @@ export { getNFTokenID, createCrossChainPayment, parseNFTokenID, + calculateHookOn, + hexNamespace, + hexHookParameters, + TTS, } diff --git a/packages/xrpl/test/client/getFeeXrp.test.ts b/packages/xrpl/test/client/getFeeXrp.test.ts index 90147f3ab0..930050193b 100644 --- a/packages/xrpl/test/client/getFeeXrp.test.ts +++ b/packages/xrpl/test/client/getFeeXrp.test.ts @@ -1,6 +1,6 @@ import { assert } from 'chai' -import getFeeXrp from '../../src/sugar/getFeeXrp' +import { getFeeXrp } from '../../src/sugar/getFeeXrp' import rippled from '../fixtures/rippled' import { setupClient, diff --git a/packages/xrpl/test/models/setHook.test.ts b/packages/xrpl/test/models/setHook.test.ts new file mode 100644 index 0000000000..4e2569dbec --- /dev/null +++ b/packages/xrpl/test/models/setHook.test.ts @@ -0,0 +1,150 @@ +import { assert } from 'chai' + +import { validate, ValidationError } from '../../src' +import { validateSetHook } from '../../src/models/transactions/setHook' + +/** + * SetHook Transaction Verification Testing. + * + * Providing runtime verification testing for each specific transaction type. + */ +describe('SetHook', function () { + let setHookTx + + beforeEach(function () { + setHookTx = { + Flags: 0, + TransactionType: 'SetHook', + Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', + Fee: '12', + Hooks: [ + { + Hook: { + CreateCode: + '0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22', + HookOn: + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFF7', + Flags: 1, + HookApiVersion: 0, + HookNamespace: + '4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9', + }, + }, + ], + } as any + }) + + it(`verifies valid SetHook`, function () { + assert.doesNotThrow(() => validateSetHook(setHookTx)) + assert.doesNotThrow(() => validate(setHookTx)) + }) + + // it(`throws w/ empty Hooks`, function () { + // setHookTx.Hooks = [] + + // assert.throws( + // () => validateSetHook(setHookTx), + // ValidationError, + // 'SetHook: need at least 1 member in Hooks', + // ) + // assert.throws( + // () => validate(setHookTx), + // ValidationError, + // 'SetHook: need at least 1 member in Hooks', + // ) + // }) + + it(`throws w/ invalid Hooks`, function () { + setHookTx.Hooks = 'khgfgyhujk' + + assert.throws( + () => validateSetHook(setHookTx), + ValidationError, + 'SetHook: invalid Hooks', + ) + assert.throws( + () => validate(setHookTx), + ValidationError, + 'SetHook: invalid Hooks', + ) + }) + + it(`throws w/ maximum of 4 members allowed in Hooks`, function () { + setHookTx.Hooks = [] + const hook = { + Hook: { + CreateCode: + '0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22', + HookOn: + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFF7', + Flags: 1, + HookApiVersion: 0, + HookNamespace: + '4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9', + }, + } + setHookTx.Hooks.push(hook) + setHookTx.Hooks.push(hook) + setHookTx.Hooks.push(hook) + setHookTx.Hooks.push(hook) + setHookTx.Hooks.push(hook) + + const errorMessage = 'SetHook: maximum of 4 hooks allowed in Hooks' + assert.throws( + () => validateSetHook(setHookTx), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(setHookTx), ValidationError, errorMessage) + }) + + it(`throws w/ invalid HookOn in Hooks`, function () { + setHookTx.SignerQuorum = 2 + setHookTx.Hooks = [ + { + Hook: { + CreateCode: + '0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22', + HookOn: '', + Flags: 1, + HookApiVersion: 0, + HookNamespace: + '4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9', + }, + }, + ] + const errorMessage = + 'SetHook: HookOn in Hook must be a 256-bit (32-byte) hexadecimal value' + assert.throws( + () => validateSetHook(setHookTx), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(setHookTx), ValidationError, errorMessage) + }) + + it(`throws w/ invalid HookNamespace in Hooks`, function () { + setHookTx.SignerQuorum = 2 + setHookTx.Hooks = [ + { + Hook: { + CreateCode: + '0061736D01000000011C0460057F7F7F7F7F017E60037F7F7E017E60027F7F017F60017F017E02230303656E76057472616365000003656E7606616363657074000103656E76025F670002030201030503010002062B077F0141B088040B7F004180080B7F0041A6080B7F004180080B7F0041B088040B7F0041000B7F0041010B07080104686F6F6B00030AC4800001C0800001017F230041106B220124002001200036020C41920841134180084112410010001A410022002000420010011A41012200200010021A200141106A240042000B0B2C01004180080B254163636570742E633A2043616C6C65642E00224163636570742E633A2043616C6C65642E22', + HookOn: + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFF7', + Flags: 1, + HookApiVersion: 0, + HookNamespace: '', + }, + }, + ] + const errorMessage = + 'SetHook: HookNamespace in Hook must be a 256-bit (32-byte) hexadecimal value' + assert.throws( + () => validateSetHook(setHookTx), + ValidationError, + errorMessage, + ) + assert.throws(() => validate(setHookTx), ValidationError, errorMessage) + }) +}) diff --git a/packages/xrpl/test/utils/hooks.test.ts b/packages/xrpl/test/utils/hooks.test.ts new file mode 100644 index 0000000000..5033527562 --- /dev/null +++ b/packages/xrpl/test/utils/hooks.test.ts @@ -0,0 +1,64 @@ +import { assert } from 'chai' + +import { + calculateHookOn, + hexNamespace, + hexHookParameters, + TTS, +} from '../../src' + +describe('test hook on', function () { + it('invalid', function () { + const invokeOn: Array = ['AccountSet1'] + expect(() => { + calculateHookOn(invokeOn) + }).toThrow("invalid transaction type 'AccountSet1' in HookOn array") + }) + it('all', function () { + const result = calculateHookOn([]) + assert.equal( + result, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF', + ) + }) + it('one', function () { + const invokeOn: Array = ['AccountSet'] + const result = calculateHookOn(invokeOn) + assert.equal( + result, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFF7', + ) + }) +}) + +describe('test hook namespace', function () { + it('basic', async function () { + const result = await hexNamespace('starter') + assert.equal( + result, + '4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9', + ) + }) +}) + +describe('test hook parameters', function () { + it('basic', async function () { + const parameters = [ + { + HookParameter: { + HookParameterName: 'name1', + HookParameterValue: 'value1', + }, + }, + ] + const result = hexHookParameters(parameters) + assert.deepEqual(result, [ + { + HookParameter: { + HookParameterName: '6E616D6531', + HookParameterValue: '76616C756531', + }, + }, + ]) + }) +})