mirror of
				https://github.com/Xahau/xahau.js.git
				synced 2025-11-04 13:05:49 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			363 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			363 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { assert } from 'chai'
 | 
						|
import omit from 'lodash/omit'
 | 
						|
import throttle from 'lodash/throttle'
 | 
						|
import { decode } from 'ripple-binary-codec'
 | 
						|
 | 
						|
import {
 | 
						|
  Client,
 | 
						|
  Wallet,
 | 
						|
  AccountInfoRequest,
 | 
						|
  type SubmitResponse,
 | 
						|
  TimeoutError,
 | 
						|
  NotConnectedError,
 | 
						|
  AccountLinesRequest,
 | 
						|
  IssuedCurrency,
 | 
						|
} from '../../src'
 | 
						|
import { Payment, Transaction } from '../../src/models/transactions'
 | 
						|
import { hashSignedTx } from '../../src/utils/hashes'
 | 
						|
 | 
						|
export const GENESIS_ACCOUNT = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'
 | 
						|
const GENESIS_SECRET = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb'
 | 
						|
 | 
						|
export async function sendLedgerAccept(client: Client): Promise<unknown> {
 | 
						|
  return client.connection.request({ command: 'ledger_accept' })
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Throttles an async function in a way that can be awaited.
 | 
						|
 * By default throttle doesn't return a promise for async functions unless it's invoking them immediately.
 | 
						|
 *
 | 
						|
 * @param func - async function to throttle calls for.
 | 
						|
 * @param wait - same function as lodash.throttle's wait parameter. Call this function at most this often.
 | 
						|
 * @returns a promise which will be resolved/ rejected only if the function is executed, with the result of the underlying call.
 | 
						|
 */
 | 
						|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Proper
 | 
						|
function asyncThrottle<F extends (...args: any[]) => Promise<unknown>>(
 | 
						|
  func: F,
 | 
						|
  wait?: number,
 | 
						|
): (...args: Parameters<F>) => ReturnType<F> {
 | 
						|
  const throttled = throttle((resolve, reject, args: Parameters<F>) => {
 | 
						|
    func(...args)
 | 
						|
      .then(resolve)
 | 
						|
      .catch(reject)
 | 
						|
  }, wait)
 | 
						|
  const ret = (...args: Parameters<F>): ReturnType<F> =>
 | 
						|
    new Promise((resolve, reject) => {
 | 
						|
      throttled(resolve, reject, args)
 | 
						|
    }) as ReturnType<F>
 | 
						|
  return ret
 | 
						|
}
 | 
						|
 | 
						|
const throttledLedgerAccept = asyncThrottle(sendLedgerAccept, 1000)
 | 
						|
 | 
						|
export async function ledgerAccept(
 | 
						|
  client: Client,
 | 
						|
  retries?: number,
 | 
						|
  shouldThrottle?: boolean,
 | 
						|
): Promise<unknown> {
 | 
						|
  return new Promise<unknown>((resolve, reject) => {
 | 
						|
    const ledgerAcceptFunc = shouldThrottle
 | 
						|
      ? throttledLedgerAccept
 | 
						|
      : sendLedgerAccept
 | 
						|
    ledgerAcceptFunc(client)
 | 
						|
      .then(resolve)
 | 
						|
      .catch((error) => {
 | 
						|
        if (retries === undefined) {
 | 
						|
          setTimeout(() => {
 | 
						|
            resolve(ledgerAccept(client, 10))
 | 
						|
          }, 1000)
 | 
						|
        } else if (retries > 0) {
 | 
						|
          setTimeout(() => {
 | 
						|
            resolve(ledgerAccept(client, retries - 1))
 | 
						|
          }, 1000)
 | 
						|
        } else {
 | 
						|
          reject(error)
 | 
						|
        }
 | 
						|
      })
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
export function subscribeDone(client: Client): void {
 | 
						|
  client.removeAllListeners()
 | 
						|
}
 | 
						|
 | 
						|
export async function submitTransaction({
 | 
						|
  client,
 | 
						|
  transaction,
 | 
						|
  wallet,
 | 
						|
  retry = { count: 5, delayMs: 1000 },
 | 
						|
}: {
 | 
						|
  client: Client
 | 
						|
  transaction: Transaction
 | 
						|
  wallet: Wallet
 | 
						|
  retry?: {
 | 
						|
    count: number
 | 
						|
    delayMs: number
 | 
						|
  }
 | 
						|
}): Promise<SubmitResponse> {
 | 
						|
  let response: SubmitResponse
 | 
						|
  try {
 | 
						|
    response = await client.submit(transaction, { wallet })
 | 
						|
 | 
						|
    // Retry if another transaction finished before this one
 | 
						|
    while (
 | 
						|
      ['tefPAST_SEQ', 'tefMAX_LEDGER'].includes(
 | 
						|
        response.result.engine_result,
 | 
						|
      ) &&
 | 
						|
      retry.count > 0
 | 
						|
    ) {
 | 
						|
      // eslint-disable-next-line no-param-reassign -- we want to decrement the count
 | 
						|
      retry.count -= 1
 | 
						|
      // eslint-disable-next-line no-await-in-loop, no-promise-executor-return -- We are waiting on retries
 | 
						|
      await new Promise((resolve) => setTimeout(resolve, retry.delayMs))
 | 
						|
      // eslint-disable-next-line no-await-in-loop -- We are retrying in a loop on purpose
 | 
						|
      response = await client.submit(transaction, { wallet })
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    if (error instanceof TimeoutError || error instanceof NotConnectedError) {
 | 
						|
      // retry
 | 
						|
      return submitTransaction({
 | 
						|
        client,
 | 
						|
        transaction,
 | 
						|
        wallet,
 | 
						|
        retry: {
 | 
						|
          ...retry,
 | 
						|
          count: retry.count > 0 ? retry.count - 1 : 0,
 | 
						|
        },
 | 
						|
      })
 | 
						|
    }
 | 
						|
 | 
						|
    throw error
 | 
						|
  }
 | 
						|
 | 
						|
  return response
 | 
						|
}
 | 
						|
 | 
						|
export async function fundAccount(
 | 
						|
  client: Client,
 | 
						|
  wallet: Wallet,
 | 
						|
  retry?: {
 | 
						|
    count: number
 | 
						|
    delayMs: number
 | 
						|
  },
 | 
						|
): Promise<SubmitResponse> {
 | 
						|
  const payment: Payment = {
 | 
						|
    TransactionType: 'Payment',
 | 
						|
    Account: GENESIS_ACCOUNT,
 | 
						|
    Destination: wallet.classicAddress,
 | 
						|
    // 2 times the amount needed for a new account (20 XRP)
 | 
						|
    Amount: '400000000',
 | 
						|
  }
 | 
						|
  const wal = Wallet.fromSeed(GENESIS_SECRET)
 | 
						|
  const response = await submitTransaction({
 | 
						|
    client,
 | 
						|
    wallet: wal,
 | 
						|
    transaction: payment,
 | 
						|
    retry,
 | 
						|
  })
 | 
						|
 | 
						|
  if (response.result.engine_result !== 'tesSUCCESS') {
 | 
						|
    // eslint-disable-next-line no-console -- happens only when something goes wrong
 | 
						|
    console.log(response)
 | 
						|
    assert.fail(`Response not successful, ${response.result.engine_result}`)
 | 
						|
  }
 | 
						|
  await ledgerAccept(client)
 | 
						|
  const signedTx = omit(response.result.tx_json, 'hash')
 | 
						|
  await verifySubmittedTransaction(client, signedTx as Transaction)
 | 
						|
  return response
 | 
						|
}
 | 
						|
 | 
						|
export async function generateFundedWallet(client: Client): Promise<Wallet> {
 | 
						|
  const wallet = Wallet.generate()
 | 
						|
  await fundAccount(client, wallet)
 | 
						|
  return wallet
 | 
						|
}
 | 
						|
 | 
						|
export async function verifySubmittedTransaction(
 | 
						|
  client: Client,
 | 
						|
  tx: Transaction | string,
 | 
						|
  hashTx?: string,
 | 
						|
): Promise<void> {
 | 
						|
  const hash = hashTx ?? hashSignedTx(tx)
 | 
						|
  const data = await client.request({
 | 
						|
    command: 'tx',
 | 
						|
    transaction: hash,
 | 
						|
  })
 | 
						|
  // 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') {
 | 
						|
    decodedTx.DeliverMax = decodedTx.Amount
 | 
						|
  }
 | 
						|
 | 
						|
  assert(data.result)
 | 
						|
  assert.deepEqual(
 | 
						|
    omit(data.result, [
 | 
						|
      'date',
 | 
						|
      'hash',
 | 
						|
      'inLedger',
 | 
						|
      'ledger_index',
 | 
						|
      'meta',
 | 
						|
      'validated',
 | 
						|
      'ctid',
 | 
						|
    ]),
 | 
						|
    decodedTx,
 | 
						|
  )
 | 
						|
  if (typeof data.result.meta === 'object') {
 | 
						|
    assert.strictEqual(data.result.meta.TransactionResult, 'tesSUCCESS')
 | 
						|
  } else {
 | 
						|
    assert.strictEqual(data.result.meta, 'tesSUCCESS')
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sends a test transaction for integration testing.
 | 
						|
 *
 | 
						|
 * @param client - The XRPL client
 | 
						|
 * @param transaction - The transaction object to send.
 | 
						|
 * @param wallet - The wallet to send the transaction from.
 | 
						|
 * @param retry - As of Sep 2022, xrpl.js does not track requests sent in parallel. Our sequence numbers can get off from
 | 
						|
 *               the server's sequence numbers. This is a fix to retry the transaction if it fails due to tefPAST_SEQ.
 | 
						|
 * @param retry.count - How many times the request should be retried.
 | 
						|
 * @param retry.delayMs - How long to wait between retries.
 | 
						|
 * @returns The response of the transaction.
 | 
						|
 */
 | 
						|
// eslint-disable-next-line max-params -- Test function, many params are needed
 | 
						|
export async function testTransaction(
 | 
						|
  client: Client,
 | 
						|
  transaction: Transaction,
 | 
						|
  wallet: Wallet,
 | 
						|
  retry?: {
 | 
						|
    count: number
 | 
						|
    delayMs: number
 | 
						|
  },
 | 
						|
): Promise<SubmitResponse> {
 | 
						|
  // Accept any un-validated changes.
 | 
						|
 | 
						|
  // sign/submit the transaction
 | 
						|
  const response = await submitTransaction({
 | 
						|
    client,
 | 
						|
    wallet,
 | 
						|
    transaction,
 | 
						|
    retry,
 | 
						|
  })
 | 
						|
 | 
						|
  // check that the transaction was successful
 | 
						|
  assert.equal(response.type, 'response')
 | 
						|
 | 
						|
  if (response.result.engine_result !== 'tesSUCCESS') {
 | 
						|
    // eslint-disable-next-line no-console -- See output
 | 
						|
    console.error(
 | 
						|
      `Transaction was not successful. Expected response.result.engine_result to be tesSUCCESS but got ${response.result.engine_result}`,
 | 
						|
    )
 | 
						|
    // eslint-disable-next-line no-console -- See output
 | 
						|
    console.error('The transaction was: ', transaction)
 | 
						|
    // eslint-disable-next-line no-console -- See output
 | 
						|
    console.error('The response was: ', JSON.stringify(response))
 | 
						|
  }
 | 
						|
 | 
						|
  assert.equal(
 | 
						|
    response.result.engine_result,
 | 
						|
    'tesSUCCESS',
 | 
						|
    response.result.engine_result_message,
 | 
						|
  )
 | 
						|
 | 
						|
  // check that the transaction is on the ledger
 | 
						|
  const signedTx = omit(response.result.tx_json, 'hash')
 | 
						|
  await ledgerAccept(client)
 | 
						|
  await verifySubmittedTransaction(client, signedTx as Transaction)
 | 
						|
  return response
 | 
						|
}
 | 
						|
 | 
						|
export async function getXRPBalance(
 | 
						|
  client: Client,
 | 
						|
  account: string | Wallet,
 | 
						|
): Promise<string> {
 | 
						|
  const address: string =
 | 
						|
    typeof account === 'string' ? account : account.classicAddress
 | 
						|
  const request: AccountInfoRequest = {
 | 
						|
    command: 'account_info',
 | 
						|
    account: address,
 | 
						|
  }
 | 
						|
  return (await client.request(request)).result.account_data.Balance
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Retrieves the close time of the ledger.
 | 
						|
 *
 | 
						|
 * @param client - The client object.
 | 
						|
 * @returns - A promise that resolves to the close time of the ledger.
 | 
						|
 *
 | 
						|
 * @example
 | 
						|
 * const closeTime = await getLedgerCloseTime(client);
 | 
						|
 * console.log(closeTime); // Output: 1626424978
 | 
						|
 */
 | 
						|
export async function getLedgerCloseTime(client: Client): Promise<number> {
 | 
						|
  const CLOSE_TIME: number = (
 | 
						|
    await client.request({
 | 
						|
      command: 'ledger',
 | 
						|
      ledger_index: 'validated',
 | 
						|
    })
 | 
						|
  ).result.ledger.close_time
 | 
						|
 | 
						|
  return CLOSE_TIME
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Waits for the ledger time to reach a specific value and forces ledger progress if necessary.
 | 
						|
 *
 | 
						|
 * @param client - The client object.
 | 
						|
 * @param ledgerTime - The target ledger time.
 | 
						|
 * @param [retries=20] - The number of retries before throwing an error.
 | 
						|
 * @returns - A promise that resolves when the ledger time reaches the target value.
 | 
						|
 *
 | 
						|
 * @example
 | 
						|
 * try {
 | 
						|
 *   await waitForAndForceProgressLedgerTime(client, 1626424978, 10);
 | 
						|
 *   console.log('Ledger time reached.'); // Output: Ledger time reached.
 | 
						|
 * } catch (error) {
 | 
						|
 *   console.error(error);
 | 
						|
 * }
 | 
						|
 */
 | 
						|
export async function waitForAndForceProgressLedgerTime(
 | 
						|
  client: Client,
 | 
						|
  ledgerTime: number,
 | 
						|
  retries = 20,
 | 
						|
): Promise<void> {
 | 
						|
  async function getCloseTime(): Promise<boolean> {
 | 
						|
    const CLOSE_TIME: number = await getLedgerCloseTime(client)
 | 
						|
    if (CLOSE_TIME >= ledgerTime) {
 | 
						|
      return true
 | 
						|
    }
 | 
						|
 | 
						|
    return false
 | 
						|
  }
 | 
						|
 | 
						|
  let retryCounter = retries || 0
 | 
						|
 | 
						|
  while (retryCounter > 0) {
 | 
						|
    // eslint-disable-next-line no-await-in-loop -- Necessary for retries
 | 
						|
    if (await getCloseTime()) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    // eslint-disable-next-line no-await-in-loop -- Necessary for retries
 | 
						|
    await ledgerAccept(client)
 | 
						|
    retryCounter -= 1
 | 
						|
  }
 | 
						|
 | 
						|
  throw new Error(`Ledger time not reached after ${retries} retries.`)
 | 
						|
}
 | 
						|
 | 
						|
export async function getIOUBalance(
 | 
						|
  client: Client,
 | 
						|
  wallet: Wallet,
 | 
						|
  currency: IssuedCurrency,
 | 
						|
): Promise<string> {
 | 
						|
  const request: AccountLinesRequest = {
 | 
						|
    command: 'account_lines',
 | 
						|
    account: wallet.classicAddress,
 | 
						|
    peer: currency.issuer,
 | 
						|
  }
 | 
						|
  return (await client.request(request)).result.lines[0].balance
 | 
						|
}
 |