Compare commits

..

8 Commits

Author SHA1 Message Date
tequ
4cd8736eb4 NamedHook Amendment (#50) 2026-05-29 22:25:12 +09:00
tequ
5e789b1d10 IOUClaimReward Amendment (#45) 2026-05-29 22:05:06 +09:00
tequ
8dbe640aa9 feat: add support for the simulate RPC (XLS-69d) (#2867) (#64) 2026-05-29 09:16:34 +00:00
tequ
4fc68a4bbf Add fields test for SetHook (#66) 2026-05-29 18:08:16 +09:00
tequ
deaf6af88b Add integration test for SetHook transaction (#65)
* Add integration test for SetHook transaction
* Update xahaud.cfg feature stanza
2026-05-27 20:43:13 +09:00
tequ
64608cb9a6 Xahaud 2026.5.26-dev+3273 (#60) 2026-05-27 20:01:37 +09:00
tequ
cc5cf1f2ea Update custom Payment to a higher number in binary codec test (#2824) (#62) 2026-05-27 12:14:29 +09:00
tequ
b417d67b28 4.0.4 (#61) 2026-05-27 11:54:01 +09:00
24 changed files with 2602 additions and 1047 deletions

View File

@@ -101,69 +101,105 @@ r.ripple.com 51235
# If you need the version of rippled to be more up to date, you may need to make a comment on this repo: https://github.com/WietseWind/docker-rippled
[features]
# Amendments
NegativeUNL
fixGuardDepth32
NamedHooks
IOURewardClaim
fixIOULockedBalanceInvariant
fixImportIssuer
HookAPISerializedType240
# PermissionedDomains Supported::no
# DynamicNFT Supported::no
# Credentials Supported::no
AMMClawback
# MPTokensV1 Supported::no
# InvariantsV1_1 Supported::no
fixNFTokenPageLinks
fixEnforceNFTokenTrustline
fixReducedOffersV2
# NFTokenMintOffer Supported::no
fixPreviousTxnID
PriceOracle
fixInnerObjTemplate
fixNFTokenReserve
fixFillOrKill
# DID Supported::no
fixDisallowIncomingV1
# XChainBridge Supported::no
AMM
fixReducedOffersV1
HooksUpdate2
HookOnV2
fixHookAPI20251128
fixCronStacking
ExtendedHookState
fixInvalidTxFlags
Cron
IOUIssuerWeakTSH
DeepFreeze
fixProvisionalDoubleThreading
Clawback
fixRewardClaimFlags
HookCanEmit
fix20250131
fixXahauV3
fixReduceImport
Touch
Remarks
fixFloatDivide
fix240911
fixPageCap
fix240819
fixNSDelete
ZeroB2M
Remit
fixXahauV2
fixXahauV1
HooksUpdate1
XahauGenesis
Import
URIToken
PaychanAndEscrowForTokens
BalanceRewards
Hooks
fixNFTokenRemint
fixNonFungibleTokensV1_2
fixUniversalNumber
XRPFees
DisallowIncoming
ImmediateOfferKilled
fixRemoveNFTokenAutoTrustLine
NonFungibleTokensV1
fixTrustLinesToSelf
NonFungibleTokensV1_1
ExpandedSignerList
CheckCashMakesTrustLine
fixRmSmallIncreasedQOffers
fixSTAmountCanonicalize
FlowSortStrands
TicketBatch
fixQualityUpperBound
FlowCross
HardenedValidations
DepositPreauth
MultiSignReserve
fix1623
fix1513
RequireFullyCanonicalSig
fix1543
fix1781
fixCheckThreading
fix1515
CryptoConditionsSuite
fixPayChanRecipientOwnerDir
fix1578
fix1571
NegativeUNL
fixAmendmentMajorityCalc
fixTakerDryOfferRemoval
fixMasterKeyAsRegularKey
Flow
HardenedValidations
fix1781
RequireFullyCanonicalSig
fixQualityUpperBound
DeletableAccounts
DepositAuth
fixPayChanRecipientOwnerDir
fixCheckThreading
fixMasterKeyAsRegularKey
fixTakerDryOfferRemoval
MultiSignReserve
fix1578
fix1515
DepositPreauth
fix1623
fix1543
fix1571
Checks
NonFungibleTokensV1_1
DisallowIncoming
fixNonFungibleTokensV1_2
fixUniversalNumber
ImmediateOfferKilled
XRPFees
ExpandedSignerList
fixNFTokenRemint
# Additional Amendments
BalanceRewards
Hooks
HooksUpdate1
Import
Remit
URIToken
XahauGenesis
ZeroB2M
fix240819
fix240911
fixFloatDivide
fixNFTokenDirV1
fixNFTokenNegOffer
fixNSDelete
fixPageCap
fixReduceImport
fixXahauV1
fixXahauV2
fixXahauV3
PaychanAndEscrowForTokens
DeepFreeze
Clawback
DepositAuth
fix1513
FlowCross
Flow
# OwnerPaysFee Supported::no
[network_id]
21337

View File

@@ -4,7 +4,7 @@
name: Node.js CI
env:
XAHAUD_VERSION: 2025.12.1-release+2609
XAHAUD_VERSION: 2026.5.26-dev+3268
on:
push:

2
package-lock.json generated
View File

@@ -16048,7 +16048,7 @@
}
},
"packages/xahau": {
"version": "4.0.4-alpha.3",
"version": "4.0.4",
"license": "ISC",
"dependencies": {
"@scure/bip32": "^1.3.1",

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,9 @@ describe('Signing data', function () {
const customPaymentDefinitions = JSON.parse(
JSON.stringify(normalDefinitions),
)
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 31
// custom number would need to updated in case it has been used by an existing transaction type
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 200
const newDefs = new XrplDefinitions(customPaymentDefinitions)
const actual = encodeForSigning(tx_json, newDefs)
@@ -82,7 +84,7 @@ describe('Signing data', function () {
'53545800', // signingPrefix
// TransactionType
'12',
'001F',
'00C8',
// Flags
'22',
'80000000',
@@ -176,7 +178,9 @@ describe('Signing data', function () {
const customPaymentDefinitions = JSON.parse(
JSON.stringify(normalDefinitions),
)
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 31
// custom number would need to updated in case it has been used by an existing transaction type
customPaymentDefinitions.TRANSACTION_TYPES.Payment = 200
const newDefs = new XrplDefinitions(customPaymentDefinitions)
const signingAccount = 'rJZdUusLDtY9NEsGea7ijqhVrXv98rYBYN'
@@ -187,7 +191,7 @@ describe('Signing data', function () {
'534D5400', // signingPrefix
// TransactionType
'12',
'001F',
'00C8',
// Flags
'22',
'80000000',

View File

@@ -4,6 +4,21 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
## Unreleased Changes
### Added
* Support for IOUClaimReward Amendment
* Support for NamedHooks
* Support for the `simulate` RPC ([XLS-69](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0069-simulate))
## 4.0.4 (2026-05-27)
### Added
* Improve HookStateScale validation
### Fixed
* Add lsfTshCollect flag in AccountRoot
* Refactor amount assignment in partialPayment.ts
* Fix setTransactionFlagsToNumber for Xahau transactions
## 4.0.3 (2025-11-18)
### Added

View File

@@ -1,6 +1,6 @@
{
"name": "xahau",
"version": "4.0.4-alpha.3",
"version": "4.0.4",
"license": "ISC",
"description": "A TypeScript/JavaScript API for interacting with the Xahau Network in Node.js and the browser",
"files": [

View File

@@ -40,8 +40,13 @@ import type {
MarkerRequest,
MarkerResponse,
SubmitResponse,
SimulateRequest,
} from '../models/methods'
import type { BookOffer, BookOfferCurrency } from '../models/methods/bookOffers'
import {
SimulateBinaryResponse,
SimulateJsonResponse,
} from '../models/methods/simulate'
import type {
EventTypes,
OnEventToListenerMap,
@@ -734,6 +739,41 @@ class Client extends EventEmitter<EventTypes> {
return submitRequest(this, signedTx, opts?.failHard)
}
/**
* Simulates an unsigned transaction.
* Steps performed on a transaction:
* 1. Autofill.
* 2. Sign & Encode.
* 3. Submit.
*
* @category Core
*
* @param transaction - A transaction to autofill, sign & encode, and submit.
* @param opts - (Optional) Options used to sign and submit a transaction.
* @param opts.binary - If true, return the metadata in a binary encoding.
*
* @returns A promise that contains SimulateResponse.
* @throws RippledError if the simulate request fails.
*/
public async simulate<Binary extends boolean = false>(
transaction: SubmittableTransaction | string,
opts?: {
// If true, return the binary-encoded representation of the results.
binary?: Binary
},
): Promise<
Binary extends true ? SimulateBinaryResponse : SimulateJsonResponse
> {
// send request
const binary = opts?.binary ?? false
const request: SimulateRequest =
typeof transaction === 'string'
? { command: 'simulate', tx_blob: transaction, binary }
: { command: 'simulate', tx_json: transaction, binary }
return this.request(request)
}
/**
* Asynchronously submits a transaction and verifies that it has been included in a
* validated ledger (or has errored/will not be included for some reason).

View File

@@ -106,6 +106,10 @@ export interface Hook {
* The grants of the hook.
*/
HookGrants?: HookGrant[]
/**
* The name of the hook.
*/
HookName?: string
}
}

View File

@@ -2,6 +2,13 @@ import { IssuedCurrencyAmount } from '../common'
import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'
export interface RippleStateReward {
RewardLgrFirst: number
RewardLgrLast: number
RewardTime: number
TrustLineRewardAccumulator: IssuedCurrencyAmount
}
/**
* The RippleState object type connects two accounts in a single currency.
*
@@ -61,6 +68,8 @@ export default interface RippleState extends BaseLedgerEntry, HasPreviousTxnID {
* equivalent to 1 billion, or face value.
*/
HighQualityOut?: number
HighReward?: RippleStateReward
LowReward?: RippleStateReward
}
export enum RippleStateFlags {

View File

@@ -133,6 +133,14 @@ import {
StateAccountingFinal,
} from './serverInfo'
import { ServerStateRequest, ServerStateResponse } from './serverState'
import {
SimulateBinaryRequest,
SimulateBinaryResponse,
SimulateJsonRequest,
SimulateJsonResponse,
SimulateRequest,
SimulateResponse,
} from './simulate'
import { SubmitRequest, SubmitResponse } from './submit'
import {
SubmitMultisignedRequest,
@@ -188,6 +196,7 @@ type Request =
| LedgerDataRequest
| LedgerEntryRequest
// transaction methods
| SimulateRequest
| SubmitRequest
| SubmitMultisignedRequest
| TransactionEntryRequest
@@ -235,6 +244,7 @@ type Response<Version extends APIVersion = typeof DEFAULT_API_VERSION> =
| LedgerDataResponse
| LedgerEntryResponse
// transaction methods
| SimulateResponse
| SubmitResponse
| SubmitMultisignedVersionResponseMap<Version>
| TransactionEntryResponse
@@ -355,6 +365,12 @@ export type RequestResponseMap<
? LedgerDataResponse
: T extends LedgerEntryRequest
? LedgerEntryResponse
: T extends SimulateBinaryRequest
? SimulateBinaryResponse
: T extends SimulateJsonRequest
? SimulateJsonResponse
: T extends SimulateRequest
? SimulateJsonResponse
: T extends SubmitRequest
? SubmitResponse
: T extends SubmitMultisignedRequest
@@ -486,6 +502,8 @@ export {
LedgerEntryRequest,
LedgerEntryResponse,
// transaction methods with types
SimulateRequest,
SimulateResponse,
SubmitRequest,
SubmitResponse,
SubmitMultisignedRequest,

View File

@@ -203,13 +203,13 @@ export interface LedgerQueueData {
}
export interface LedgerBinary
extends Omit<Omit<Ledger, 'transactions'>, 'accountState'> {
extends Omit<Ledger, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}
export interface LedgerBinaryV1
extends Omit<Omit<LedgerV1, 'transactions'>, 'accountState'> {
extends Omit<LedgerV1, 'transactions' | 'accountState'> {
accountState?: string[]
transactions?: string[]
}

View File

@@ -0,0 +1,88 @@
import {
BaseTransaction,
Transaction,
TransactionMetadata,
} from '../transactions'
import { BaseRequest, BaseResponse } from './baseMethod'
/**
* The `simulate` method simulates a transaction without submitting it to the network.
* Returns a {@link SimulateResponse}.
*
* @category Requests
*/
export type SimulateRequest = BaseRequest & {
command: 'simulate'
binary?: boolean
} & (
| {
tx_blob: string
tx_json?: never
}
| {
tx_json: Transaction
tx_blob?: never
}
)
export type SimulateBinaryRequest = SimulateRequest & {
binary: true
}
export type SimulateJsonRequest = SimulateRequest & {
binary?: false
}
/**
* Response expected from an {@link SimulateRequest}.
*
* @category Responses
*/
export type SimulateResponse = SimulateJsonResponse | SimulateBinaryResponse
export interface SimulateBinaryResponse extends BaseResponse {
result: {
applied: false
engine_result: string
engine_result_code: number
engine_result_message: string
tx_blob: string
meta_blob: string
/**
* The ledger index of the ledger version that was used to generate this
* response.
*/
ledger_index: number
}
}
export interface SimulateJsonResponse<T extends BaseTransaction = Transaction>
extends BaseResponse {
result: {
applied: false
engine_result: string
engine_result_code: number
engine_result_message: string
/**
* The ledger index of the ledger version that was used to generate this
* response.
*/
ledger_index: number
tx_json: T
meta?: TransactionMetadata<T>
}
}

View File

@@ -1,4 +1,5 @@
import { ValidationError } from '../../errors'
import { Currency } from '../common'
import { BaseTransaction, GlobalFlags, validateBaseTransaction } from './common'
/**
@@ -56,6 +57,7 @@ export interface ClaimReward extends BaseTransaction {
Flags?: number | ClaimRewardFlagsInterface
/** The unique address of the issuer where the reward.c hook is installed. */
Issuer?: string
ClaimCurrency?: Currency
}
/**

View File

@@ -275,6 +275,10 @@ export interface BaseTransaction {
* The hook parameters of the transaction.
*/
HookParameters?: HookParameter[]
/**
* The name of the hooks triggered by the transaction.
*/
HookName?: string
/**
* The hook parameters of the transaction.
*/

View File

@@ -18,6 +18,10 @@ export interface SetHook extends BaseTransaction {
const MAX_HOOKS = 10
const HEX_REGEX = /^[0-9A-Fa-f]{64}$/u
/**
* 4-16 bytes in hex
*/
const HOOKNAME_REGEX = /^[0-9A-Fa-f]{8,32}$/u
/**
* Verify the form and type of an SetHook at runtime.
@@ -41,7 +45,7 @@ export function validateSetHook(tx: Record<string, unknown>): void {
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, HookCanEmit, HookNamespace } = hookObject.Hook
const { HookOn, HookCanEmit, HookNamespace, HookName } = 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`,
@@ -57,5 +61,10 @@ export function validateSetHook(tx: Record<string, unknown>): void {
`SetHook: HookNamespace in Hook must be a 256-bit (32-byte) hexadecimal value`,
)
}
if (HookName !== undefined && !HOOKNAME_REGEX.test(HookName)) {
throw new ValidationError(
`SetHook: HookName in Hook must be a hex string of 8-32 hex characters`,
)
}
}
}

View File

@@ -1,5 +1,3 @@
import { decode, encode } from 'xahau-binary-codec'
import type {
Client,
SubmitRequest,
@@ -12,6 +10,7 @@ import { ValidationError, XahlError } from '../errors'
import { Signer } from '../models/common'
import { TxResponse } from '../models/methods'
import { BaseTransaction } from '../models/transactions/common'
import { decode, encode } from '../utils'
/** Approximate time for a ledger to close, in milliseconds */
const LEDGER_CLOSE_TIME = 1000
@@ -52,7 +51,7 @@ export async function submitRequest(
failHard = false,
): Promise<SubmitResponse> {
if (!isSigned(signedTransaction)) {
throw new ValidationError('Transaction must be signed')
throw new ValidationError('Transaction must be signed.')
}
const signedTxEncoded =

View File

@@ -128,6 +128,7 @@ describe('server_info (xahaud)', function () {
'node_size',
'initial_sync_duration_us',
'ports',
'git',
]
assert.deepEqual(
omit(response.result.info, removeKeys),

View File

@@ -118,6 +118,7 @@ describe('server_state', function () {
'node_size',
'initial_sync_duration_us',
'ports',
'git',
]
assert.deepEqual(
omit(response.result.state, removeKeys),

View File

@@ -0,0 +1,85 @@
import { assert } from 'chai'
import { AccountSet, SimulateRequest } from '../../../src'
import { SimulateBinaryRequest } from '../../../src/models/methods/simulate'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
// how long before each test case times out
const TIMEOUT = 20000
describe('simulate', function () {
let testContext: XrplIntegrationTestContext
beforeEach(async () => {
testContext = await setupClient(serverUrl)
})
afterEach(async () => teardownClient(testContext))
it(
'json',
async () => {
const simulateRequest: SimulateRequest = {
command: 'simulate',
tx_json: {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
NFTokenMinter: testContext.wallet.address,
},
}
const simulateResponse = await testContext.client.request(simulateRequest)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta, 'object')
assert.typeOf(simulateResponse.result.tx_json, 'object')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
it(
'binary',
async () => {
const simulateRequest: SimulateBinaryRequest = {
command: 'simulate',
tx_json: {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
},
binary: true,
}
const simulateResponse = await testContext.client.request(simulateRequest)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta_blob, 'string')
assert.typeOf(simulateResponse.result.tx_blob, 'string')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
it(
'sugar',
async () => {
const tx: AccountSet = {
TransactionType: 'AccountSet',
Account: testContext.wallet.address,
NFTokenMinter: testContext.wallet.address,
}
const simulateResponse = await testContext.client.simulate(tx)
assert.equal(simulateResponse.type, 'response')
assert.typeOf(simulateResponse.result.meta, 'object')
assert.typeOf(simulateResponse.result.tx_json, 'object')
assert.equal(simulateResponse.result.engine_result, 'tesSUCCESS')
assert.isFalse(simulateResponse.result.applied)
},
TIMEOUT,
)
})

View File

@@ -1,24 +1,66 @@
import { assert } from 'chai'
import { ClaimReward, ClaimRewardFlags } from '../../../src'
import {
ClaimReward,
ClaimRewardFlags,
SetHook,
TrustSet,
Wallet,
} from '../../../src'
import { RippleState } from '../../../src/models/ledger'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { testTransaction } from '../utils'
import { generateFundedWallet, testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
describe('ClaimReward', function () {
let testContext: XrplIntegrationTestContext
const acceptHook =
'0061736D0100000001130360027F7F017F60037F7F7E017E60017F017E02170203656E76025F67000003656E760661636365707400010302010205030100020621057F01418088040B7F004180080B7F004180080B7F00418088040B7F004180080B07080104686F6F6B00020A9D80000199800000410141011080808080001A4100410042001081808080000B'
beforeEach(async () => {
describe('XAH ClaimReward', function () {
let testContext: XrplIntegrationTestContext
let genesisWallet: Wallet
beforeAll(async () => {
genesisWallet = new Wallet(
'0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020',
'001ACAAEDECE405B2A958212629E16F2EB46B153EEE94CDD350FDEFF52795525B7',
)
testContext = await setupClient(serverUrl)
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: genesisWallet.classicAddress,
Hooks: [
{
Hook: {
CreateCode: acceptHook,
HookApiVersion: 0,
HookOn: '00'.repeat(32),
HookNamespace: '00'.repeat(32),
},
},
],
}
await testTransaction(testContext.client, setHookTx, genesisWallet)
})
afterAll(async () => {
// reset Hook
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: genesisWallet.classicAddress,
Hooks: [{ Hook: { CreateCode: '', Flags: { hsfOverride: true } } }],
}
await testTransaction(testContext.client, setHookTx, genesisWallet)
await teardownClient(testContext)
})
afterEach(async () => teardownClient(testContext))
it(
'opt in',
@@ -26,7 +68,7 @@ describe('ClaimReward', function () {
const tx: ClaimReward = {
TransactionType: 'ClaimReward',
Account: testContext.wallet.classicAddress,
Issuer: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
Issuer: genesisWallet.address,
}
await testTransaction(testContext.client, tx, testContext.wallet)
@@ -42,6 +84,7 @@ describe('ClaimReward', function () {
},
TIMEOUT,
)
it(
'opt out',
async () => {
@@ -67,3 +110,135 @@ describe('ClaimReward', function () {
TIMEOUT,
)
})
describe('IOU ClaimReward', function () {
let testContext: XrplIntegrationTestContext
let issuerWallet: Wallet
let hookWallet: Wallet
beforeAll(async () => {
testContext = await setupClient(serverUrl)
issuerWallet = await generateFundedWallet(testContext.client)
hookWallet = await generateFundedWallet(testContext.client)
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: hookWallet.classicAddress,
Hooks: [
{
Hook: {
CreateCode: acceptHook,
HookApiVersion: 0,
HookOn: '00'.repeat(32),
HookNamespace: '00'.repeat(32),
},
},
],
}
await testTransaction(testContext.client, setHookTx, hookWallet)
const trustSetTx: TrustSet = {
TransactionType: 'TrustSet',
Account: testContext.wallet.classicAddress,
LimitAmount: {
currency: 'USD',
issuer: issuerWallet.address,
value: '10000000',
},
}
await testTransaction(testContext.client, trustSetTx, testContext.wallet)
})
afterAll(async () => {
// reset Hook
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: hookWallet.classicAddress,
Hooks: [{ Hook: { CreateCode: '', Flags: { hsfOverride: true } } }],
}
await testTransaction(testContext.client, setHookTx, hookWallet)
await teardownClient(testContext)
})
it(
'opt in',
async () => {
const tx: ClaimReward = {
TransactionType: 'ClaimReward',
Account: testContext.wallet.classicAddress,
Issuer: hookWallet.classicAddress,
ClaimCurrency: {
currency: 'USD',
issuer: issuerWallet.address,
},
}
await testTransaction(testContext.client, tx, testContext.wallet)
const rippleStateResponse = await testContext.client.request({
command: 'ledger_entry',
ripple_state: {
currency: 'USD',
accounts: [
testContext.wallet.classicAddress,
issuerWallet.classicAddress,
],
},
})
const node = rippleStateResponse.result.node as RippleState
assert.exists(node)
// Either LowReward or HighReward must exist
expect(Boolean(node.LowReward) || Boolean(node.HighReward))
if (node.LowReward) {
assert.exists(node.LowReward.TrustLineRewardAccumulator)
assert.exists(node.LowReward.RewardLgrFirst)
assert.exists(node.LowReward.RewardLgrLast)
assert.exists(node.LowReward.RewardTime)
}
if (node.HighReward) {
assert.exists(node.HighReward.TrustLineRewardAccumulator)
assert.exists(node.HighReward.RewardLgrFirst)
assert.exists(node.HighReward.RewardLgrLast)
assert.exists(node.HighReward.RewardTime)
}
},
TIMEOUT,
)
it(
'opt out',
async () => {
const tx: ClaimReward = {
TransactionType: 'ClaimReward',
Account: testContext.wallet.classicAddress,
Flags: ClaimRewardFlags.tfOptOut,
ClaimCurrency: {
currency: 'USD',
issuer: issuerWallet.address,
},
}
await testTransaction(testContext.client, tx, testContext.wallet)
const rippleStateResponse = await testContext.client.request({
command: 'ledger_entry',
ripple_state: {
currency: 'USD',
accounts: [
testContext.wallet.classicAddress,
issuerWallet.classicAddress,
],
},
})
const node = rippleStateResponse.result.node as RippleState
assert.exists(node)
assert.notExists(node.LowReward)
assert.notExists(node.HighReward)
},
TIMEOUT,
)
})

View File

@@ -0,0 +1,96 @@
import { SetHook, Wallet } from '../../../src'
import { Hook, HookDefinition } from '../../../src/models/ledger'
import serverUrl from '../serverUrl'
import {
setupClient,
teardownClient,
type XrplIntegrationTestContext,
} from '../setup'
import { generateFundedWallet, testTransaction } from '../utils'
// how long before each test case times out
const TIMEOUT = 20000
const acceptHook =
'0061736D0100000001130360027F7F017F60037F7F7E017E60017F017E02170203656E76025F67000003656E760661636365707400010302010205030100020621057F01418088040B7F004180080B7F004180080B7F00418088040B7F004180080B07080104686F6F6B00020A9D80000199800000410141011080808080001A4100410042001081808080000B'
describe('SetHook', function () {
let testContext: XrplIntegrationTestContext
let wallet: Wallet
beforeEach(async () => {
testContext = await setupClient(serverUrl)
wallet = await generateFundedWallet(testContext.client)
})
afterEach(async () => {
// reset Hook
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: wallet.classicAddress,
Hooks: [{ Hook: { CreateCode: '', Flags: { hsfOverride: true } } }],
}
await testTransaction(testContext.client, setHookTx, wallet)
await teardownClient(testContext)
})
it(
'base',
async () => {
const setHookTx: SetHook = {
TransactionType: 'SetHook',
Account: wallet.classicAddress,
Hooks: [
{
Hook: {
CreateCode: acceptHook,
HookApiVersion: 0,
HookOn: '00'.repeat(32),
HookCanEmit: '00'.repeat(32),
HookName: '484F4F4B',
HookParameters: [
{
HookParameter: {
HookParameterName: 'DEADBEEF',
HookParameterValue: 'DEADBEEF',
},
},
],
HookNamespace: '00'.repeat(32),
},
},
],
}
await testTransaction(testContext.client, setHookTx, wallet)
const ledgerEntryResponse = await testContext.client.request({
command: 'ledger_entry',
hook: { account: wallet.classicAddress },
})
const node = ledgerEntryResponse.result.node as Hook
expect(node.Hooks.length).toEqual(1)
const hook = node.Hooks[0].Hook
expect(Object.keys(hook).length).toEqual(2)
expect(hook.HookHash).toBeDefined()
expect(hook.HookName).toBeDefined()
const hookHash = hook.HookHash!
const hookDefinitionResponse = await testContext.client.request({
command: 'ledger_entry',
hook_definition: hookHash,
})
const hookDefinitionNode = hookDefinitionResponse.result
.node as HookDefinition
expect(hookDefinitionNode.HookHash).toEqual(hookHash)
expect(hookDefinitionNode.CreateCode).toEqual(acceptHook)
expect(hookDefinitionNode.HookApiVersion).toEqual(0)
expect(hookDefinitionNode.HookOn).toEqual('00'.repeat(32))
expect(hookDefinitionNode.HookNamespace).toEqual('00'.repeat(32))
// @ts-expect-error - HookName is not defined in HookDefinition
expect(hookDefinitionNode.HookName).toBeUndefined()
expect(hookDefinitionNode.HookParameters?.length).toEqual(1)
const parameter = hookDefinitionNode.HookParameters![0].HookParameter
expect(parameter.HookParameterName).toEqual('DEADBEEF')
expect(parameter.HookParameterValue).toEqual('DEADBEEF')
},
TIMEOUT,
)
})

View File

@@ -13,6 +13,8 @@ import {
ECDSA,
AccountLinesRequest,
IssuedCurrency,
XAHAUD_API_V2,
TxResponse,
} from '../../src'
import {
Payment,
@@ -184,20 +186,22 @@ export async function verifySubmittedTransaction(
hashTx?: string,
): Promise<void> {
const hash = hashTx ?? hashSignedTx(tx)
const data = await client.request({
const data: TxResponse = await client.request({
command: 'tx',
transaction: hash,
// The current default version is v1, but we'll be using v2 for this test.
api_version: XAHAUD_API_V2,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: handle this API change for 2.0.0
const decodedTx: any = typeof tx === 'string' ? decode(tx) : tx
if (decodedTx.TransactionType === 'Payment' && client.apiVersion !== 1) {
if (decodedTx.TransactionType === 'Payment') {
decodedTx.DeliverMax = decodedTx.Amount
delete decodedTx.Amount
}
assert(data.result)
assert.deepEqual(
omit(data.result, [
omit(data.result.tx_json, [
'ctid',
'date',
'hash',

View File

@@ -32,6 +32,7 @@ describe('SetHook', function () {
HookApiVersion: 0,
HookNamespace:
'4FF9961269BF7630D32E15276569C94470174A5DA79FA567C0F62251AA9A36B9',
HookName: 'DEADBEEF',
},
},
],
@@ -168,4 +169,25 @@ describe('SetHook', function () {
)
assert.throws(() => validate(setHookTx), ValidationError, errorMessage)
})
it.each(['', '0'.repeat(7), '0'.repeat(33), 'ZZZZZZZZ'])(
`throws w/ invalid HookName in Hooks: %s`,
function (value: string) {
setHookTx.Hooks = [
{
Hook: {
HookName: value,
},
},
]
const errorMessage =
'SetHook: HookName in Hook must be a hex string of 8-32 hex characters'
assert.throws(
() => validateSetHook(setHookTx),
ValidationError,
errorMessage,
)
assert.throws(() => validate(setHookTx), ValidationError, errorMessage)
},
)
})