UNPKG

xrpl

Version:

A TypeScript/JavaScript API for interacting with the XRP Ledger in Node.js and the browser

294 lines (274 loc) 10.8 kB
import type { Client, SubmitRequest, SubmitResponse, SubmittableTransaction, Transaction, Wallet, } from '..' import { ValidationError, XrplError } 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 async function sleep(ms: number): Promise<void> { return new Promise((resolve) => { setTimeout(resolve, ms) }) } // Helper functions /** * Submits a request to the client with a signed transaction. * * @param client - The client to submit the request to. * @param signedTransaction - The signed transaction to submit. It can be either a Transaction object or a * string (encode from ripple-binary-codec) representation of the transaction. * @param [failHard=false] - Optional. Determines whether the submission should fail hard (true) or not (false). Default is false. * @returns A promise that resolves with the response from the client. * @throws {ValidationError} If the signed transaction is not valid (not signed). * * @example * import { Client } from "xrpl" * const client = new Client("wss://s.altnet.rippletest.net:51233"); * await client.connect(); * const signedTransaction = createSignedTransaction(); * // Example 1: Submitting a Transaction object * const response1 = await submitRequest(client, signedTransaction); * * // Example 2: Submitting a string representation of the transaction * const signedTransactionString = encode(signedTransaction); * const response2 = await submitRequest(client, signedTransactionString, true); */ export async function submitRequest( client: Client, signedTransaction: SubmittableTransaction | string, failHard = false, ): Promise<SubmitResponse> { if (!isSigned(signedTransaction)) { throw new ValidationError('Transaction must be signed.') } const signedTxEncoded = typeof signedTransaction === 'string' ? signedTransaction : encode(signedTransaction) const request: SubmitRequest = { command: 'submit', tx_blob: signedTxEncoded, fail_hard: isAccountDelete(signedTransaction) || failHard, } return client.request(request) } /** * Waits for the final outcome of a transaction by polling the ledger until the result can be considered final, * meaning it has either been included in a validated ledger, or the transaction's lastLedgerSequence has been * surpassed by the latest ledger sequence (meaning it will never be included in a validated ledger). * * @template T - The type of the transaction. Defaults to `Transaction`. * @param client - The client to use for requesting transaction information. * @param txHash - The hash of the transaction to wait for. * @param lastLedger - The last ledger sequence of the transaction. * @param submissionResult - The preliminary result of the transaction. * @returns A promise that resolves with the final transaction response. * * @throws {XrplError} If the latest ledger sequence surpasses the transaction's lastLedgerSequence. * * @example * import { hashes, Client } from "xrpl" * const client = new Client("wss://s.altnet.rippletest.net:51233") * await client.connect() * * const transaction = createTransaction() // your transaction function * * const signedTx = await getSignedTx(this, transaction) * * const lastLedger = getLastLedgerSequence(signedTx) * * if (lastLedger == null) { * throw new ValidationError( * 'Transaction must contain a LastLedgerSequence value for reliable submission.', * ) * } * * const response = await submitRequest(this, signedTx, opts?.failHard) * * const txHash = hashes.hashSignedTx(signedTx) * return waitForFinalTransactionOutcome( * this, * txHash, * lastLedger, * response.result.engine_result, * ) */ // eslint-disable-next-line max-params, max-lines-per-function -- this function needs to display and do with more information. export async function waitForFinalTransactionOutcome< T extends BaseTransaction = SubmittableTransaction, >( client: Client, txHash: string, lastLedger: number, submissionResult: string, ): Promise<TxResponse<T>> { await sleep(LEDGER_CLOSE_TIME) const latestLedger = await client.getLedgerIndex() if (lastLedger < latestLedger) { throw new XrplError( `The latest ledger sequence ${latestLedger} is greater than the transaction's LastLedgerSequence (${lastLedger}).\n` + `Preliminary result: ${submissionResult}`, ) } const txResponse = await client .request({ command: 'tx', transaction: txHash, }) .catch(async (error) => { // error is of an unknown type and hence we assert type to extract the value we need. // eslint-disable-next-line @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-unsafe-member-access -- ^ const message = error?.data?.error as string if (message === 'txnNotFound') { return waitForFinalTransactionOutcome<T>( client, txHash, lastLedger, submissionResult, ) } throw new Error( `${message} \n Preliminary result: ${submissionResult}.\nFull error details: ${String( error, )}`, ) }) if (txResponse.result.validated) { // TODO: resolve the type assertion below // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we know that txResponse is of type TxResponse return txResponse as TxResponse<T> } return waitForFinalTransactionOutcome<T>( client, txHash, lastLedger, submissionResult, ) } // checks if the transaction has been signed function isSigned(transaction: SubmittableTransaction | string): boolean { const tx = typeof transaction === 'string' ? decode(transaction) : transaction if (typeof tx === 'string') { return false } if (tx.Signers != null) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we know that tx.Signers is an array of Signers const signers = tx.Signers as Signer[] for (const signer of signers) { if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- necessary check signer.Signer.SigningPubKey == null || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- necessary check signer.Signer.TxnSignature == null ) { return false } } return true } return tx.SigningPubKey != null && tx.TxnSignature != null } /** * Updates a transaction with `autofill` then signs it if it is unsigned. * * @param client - The client from which to retrieve the signed transaction. * @param transaction - The transaction to retrieve. It can be either a Transaction object or * a string (encode from ripple-binary-codec) representation of the transaction. * @param [options={}] - Optional. Additional options for retrieving the signed transaction. * @param [options.autofill=true] - Optional. Determines whether the transaction should be autofilled (true) * or not (false). Default is true. * @param [options.wallet] - Optional. A wallet to sign the transaction. It must be provided when submitting * an unsigned transaction. Default is undefined. * @returns A promise that resolves with the signed transaction. * * @throws {ValidationError} If the transaction is not signed and no wallet is provided. * * @example * import { Client } from "xrpl" * import { encode } from "ripple-binary-codec" * * const client = new Client("wss://s.altnet.rippletest.net:51233"); * await client.connect(): * const transaction = createTransaction(); // createTransaction is your function to create a transaction * const options = { * autofill: true, * wallet: myWallet, * }; * * // Example 1: Retrieving a signed Transaction object * const signedTx1 = await getSignedTx(client, transaction, options); * * // Example 2: Retrieving a string representation of the signed transaction * const signedTxString = await getSignedTx(client, encode(transaction), options); */ export async function getSignedTx( client: Client, transaction: SubmittableTransaction | string, { autofill = true, wallet, }: { // If true, autofill a transaction. autofill?: boolean // A wallet to sign a transaction. It must be provided when submitting an unsigned transaction. wallet?: Wallet } = {}, ): Promise<SubmittableTransaction | string> { if (isSigned(transaction)) { return transaction } if (!wallet) { throw new ValidationError( 'Wallet must be provided when submitting an unsigned transaction', ) } let tx = typeof transaction === 'string' ? // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- converts JsonObject to correct Transaction type (decode(transaction) as unknown as SubmittableTransaction) : transaction if (autofill) { tx = await client.autofill(tx) } return wallet.sign(tx).tx_blob } // checks if there is a LastLedgerSequence as a part of the transaction /** * Retrieves the last ledger sequence from a transaction. * * @param transaction - The transaction to retrieve the last ledger sequence from. It can be either a Transaction object or * a string (encode from ripple-binary-codec) representation of the transaction. * @returns The last ledger sequence of the transaction, or null if not available. * * @example * const transaction = createTransaction(); // your function to create a transaction * * // Example 1: Retrieving the last ledger sequence from a Transaction object * const lastLedgerSequence1 = getLastLedgerSequence(transaction); * console.log(lastLedgerSequence1); // Output: 12345 * * // Example 2: Retrieving the last ledger sequence from a string representation of the transaction * const transactionString = encode(transaction); * const lastLedgerSequence2 = getLastLedgerSequence(transactionString); * console.log(lastLedgerSequence2); // Output: 67890 */ export function getLastLedgerSequence( transaction: Transaction | string, ): number | null { const tx = typeof transaction === 'string' ? decode(transaction) : transaction // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- converts LastLedgerSeq to number if present. return tx.LastLedgerSequence as number | null } // checks if the transaction is an AccountDelete transaction function isAccountDelete(transaction: Transaction | string): boolean { const tx = typeof transaction === 'string' ? decode(transaction) : transaction return tx.TransactionType === 'AccountDelete' }