From ed23739a8dcb4383e1b4749dc218980f4aba61b7 Mon Sep 17 00:00:00 2001 From: Omar Khan Date: Fri, 10 Sep 2021 17:49:34 -0400 Subject: [PATCH] add submit transaction methods (#1611) Adds submit transaction methods: submitTransaction and submitSignedTransaction. --- src/client/index.ts | 6 +++ src/ledger/submit.ts | 69 +++++++++++++++++++++++++ test/client/submitSignedTransaction.ts | 71 ++++++++++++++++++++++++++ test/client/submitTransaction.ts | 45 ++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 src/ledger/submit.ts create mode 100644 test/client/submitSignedTransaction.ts create mode 100644 test/client/submitTransaction.ts diff --git a/src/client/index.ts b/src/client/index.ts index b7b9f52d..6405bef9 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -26,6 +26,7 @@ import autofill from '../ledger/autofill' import getBalances from '../ledger/balances' import { getOrderbook, formatBidsAndAsks } from '../ledger/orderbook' import getPaths from '../ledger/pathfind' +import { submitTransaction, submitSignedTransaction } from '../ledger/submit' import getTrustlines from '../ledger/trustlines' import { clamp } from '../ledger/utils' import { @@ -160,6 +161,7 @@ function getCollectKeyFromCommand(command: string): string | null { * @param params - Parameters to prepend to a function. * @returns A function bound with params. */ +// TODO Need to refactor prepend so TS can infer the correct function signature type // eslint-disable-next-line @typescript-eslint/ban-types -- expected param types function prepend(func: Function, ...params: unknown[]): Function { // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- safe to return @@ -536,6 +538,10 @@ class Client extends EventEmitter { // @deprecated Use autofill instead public prepareTransaction = prepend(autofill, this) + public submitTransaction = prepend(submitTransaction, this) + + public submitSignedTransaction = prepend(submitSignedTransaction, this) + public getFee = getFee public getTrustlines = getTrustlines diff --git a/src/ledger/submit.ts b/src/ledger/submit.ts new file mode 100644 index 00000000..308d20a6 --- /dev/null +++ b/src/ledger/submit.ts @@ -0,0 +1,69 @@ +import { decode, encode } from 'ripple-binary-codec' + +import type { Client, SubmitRequest, SubmitResponse, Wallet } from '..' +import { ValidationError } from '../common/errors' +import { Transaction } from '../models/transactions' +import { sign } from '../wallet/signer' + +import autofill from './autofill' + +/** + * Submits an unsigned transaction. + * Steps performed on a transaction: + * 1. Autofill. + * 2. Sign & Encode. + * 3. Submit. + * + * @param client - A Client. + * @param wallet - A Wallet to sign a transaction. + * @param transaction - A transaction to autofill, sign & encode, and submit. + * @returns A promise that contains SubmitResponse. + * @throws RippledError if submit request fails. + */ +async function submitTransaction( + client: Client, + wallet: Wallet, + transaction: Transaction, +): Promise { + // TODO: replace with client.autofill(transaction) once prepend refactor is fixed. + const tx = await autofill(client, transaction) + const signedTxEncoded = sign(wallet, tx) + return submitSignedTransaction(client, signedTxEncoded) +} + +/** + * Encodes and submits a signed transaction. + * + * @param client - A Client. + * @param signedTransaction - A signed transaction to encode (if not already) and submit. + * @returns A promise that contains SubmitResponse. + * @throws RippledError if submit request fails. + */ +async function submitSignedTransaction( + client: Client, + signedTransaction: Transaction | string, +): Promise { + 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, + } + return client.request(request) +} + +function isSigned(transaction: Transaction | string): boolean { + const tx = typeof transaction === 'string' ? decode(transaction) : transaction + return ( + typeof tx !== 'string' && + (tx.SigningPubKey != null || tx.TxnSignature != null) + ) +} + +export { submitTransaction, submitSignedTransaction } diff --git a/test/client/submitSignedTransaction.ts b/test/client/submitSignedTransaction.ts new file mode 100644 index 00000000..213d072a --- /dev/null +++ b/test/client/submitSignedTransaction.ts @@ -0,0 +1,71 @@ +import { assert } from 'chai' + +import { ValidationError } from 'xrpl-local/common/errors' +import { Transaction } from 'xrpl-local/models/transactions' + +import rippled from '../fixtures/rippled' +import { setupClient, teardownClient } from '../setupClient' +import { assertRejects } from '../testUtils' + +describe('client.submitSignedTransaction', function () { + beforeEach(setupClient) + afterEach(teardownClient) + + const signedTransaction: Transaction = { + TransactionType: 'Payment', + Sequence: 1, + LastLedgerSequence: 12312, + Amount: '20000000', + Fee: '12', + SigningPubKey: + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D', + TxnSignature: + '3045022100B3D311371EDAB371CD8F2B661A04B800B61D4B132E09B7B0712D3B2F11B1758302203906B44C4A150311D74FF6A35B146763C0B5B40AC30BD815113F058AA17B3E63', + Account: 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc', + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + } + + it('should submit a signed transaction', async function () { + const signedTx: Transaction = { ...signedTransaction } + + this.mockRippled.addResponse('submit', rippled.submit.success) + + try { + const response = await this.client.submitSignedTransaction(signedTx) + assert(response.result.engine_result, 'tesSUCCESS') + } catch (_error) { + assert(false, 'Did not expect an error to be thrown') + } + }) + + it("should submit a signed transaction that's already encoded", async function () { + const signedTxEncoded = + '1200002400000001201B00003018614000000001312D0068400000000000000C7321030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D74473045022100B3D311371EDAB371CD8F2B661A04B800B61D4B132E09B7B0712D3B2F11B1758302203906B44C4A150311D74FF6A35B146763C0B5B40AC30BD815113F058AA17B3E6381142AF1861DEC1316AEEC995C94FF9E2165B1B784608314FDB08D07AAA0EB711793A3027304D688E10C3648' + + this.mockRippled.addResponse('submit', rippled.submit.success) + + try { + const response = await this.client.submitSignedTransaction( + signedTxEncoded, + ) + assert(response.result.engine_result, 'tesSUCCESS') + } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- error type thrown can be any + assert(false, `Did not expect an error to be thrown: ${error}`) + } + }) + + it('should throw a ValidationError when submitting an unsigned transaction', async function () { + const signedTx: Transaction = { ...signedTransaction } + delete signedTx.SigningPubKey + delete signedTx.TxnSignature + + this.mockRippled.addResponse('submit', rippled.submit.success) + + assertRejects( + this.client.submitSignedTransaction(signedTx), + ValidationError, + 'Transaction must be signed', + ) + }) +}) diff --git a/test/client/submitTransaction.ts b/test/client/submitTransaction.ts new file mode 100644 index 00000000..34aeb474 --- /dev/null +++ b/test/client/submitTransaction.ts @@ -0,0 +1,45 @@ +/* eslint-disable mocha/no-hooks-for-single-case -- expected for setupClient & teardownClient */ +import { assert } from 'chai' + +import { Transaction } from 'xrpl-local/models/transactions' +import Wallet from 'xrpl-local/wallet' + +import rippled from '../fixtures/rippled' +import { setupClient, teardownClient } from '../setupClient' + +describe('client.submitTransaction', function () { + beforeEach(setupClient) + afterEach(teardownClient) + + const publicKey = + '030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D' + const privateKey = + '00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F' + const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc' + + it('should submit an unsigned transaction', async function () { + const tx: Transaction = { + TransactionType: 'Payment', + Account: address, + Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r', + Amount: '20000000', + Sequence: 1, + Fee: '12', + LastLedgerSequence: 12312, + } + const wallet = new Wallet(publicKey, privateKey) + + this.mockRippled.addResponse('account_info', rippled.account_info.normal) + this.mockRippled.addResponse('ledger', rippled.ledger.normal) + this.mockRippled.addResponse('server_info', rippled.server_info.normal) + this.mockRippled.addResponse('submit', rippled.submit.success) + + try { + const response = await this.client.submitTransaction(wallet, tx) + assert(response.result.engine_result, 'tesSUCCESS') + } catch (error) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- error type thrown can be any + assert(false, `Did not expect an error to be thrown: ${error}`) + } + }) +})