refactor: simplify submit transaction methods (#1725)

* To simplify submit transaction requests by having only one version of them that supports both signed/unsigned transactions, the signed versions of them (submitSigned(), submitSignedReliable()) are deleted.

* Their signed logic is merged into submit() and submitAndWait() (renamed from submitReliable()).

* Change order of submit method params to be consistent.

* Add a SubmitOptions method param to include options for wallet to sign a transaction, and booleans to autofill/failHard a transaction.
This commit is contained in:
Omar Khan
2021-10-15 19:10:35 -04:00
committed by GitHub
parent a2f7fe6e23
commit 09a0f2bbcb
14 changed files with 238 additions and 225 deletions

File diff suppressed because one or more lines are too long

View File

@@ -93,9 +93,7 @@ import {
getBalances,
getXrpBalance,
submit,
submitSigned,
submitReliable,
submitSignedReliable,
submitAndWait,
} from '../sugar'
import fundWallet from '../wallet/fundWallet'
@@ -596,15 +594,7 @@ class Client extends EventEmitter {
/**
* @category Core
*/
public submitSigned = submitSigned
/**
* @category Core
*/
public submitReliable = submitReliable
/**
* @category Core
*/
public submitSignedReliable = submitSignedReliable
public submitAndWait = submitAndWait
/**
* @deprecated Use autofill instead, provided for users familiar with v1

View File

@@ -15,55 +15,38 @@ async function sleep(ms: number): Promise<void> {
})
}
interface SubmitOptions {
// If true, autofill a transaction.
autofill?: boolean
// If true, and the transaction fails locally, do not retry or relay the transaction to other servers.
failHard?: boolean
// A wallet to sign a transaction. It must be provided when submitting an unsigned transaction.
wallet?: Wallet
}
/**
* Submits an unsigned transaction.
* Submits a signed/unsigned transaction.
* Steps performed on a transaction:
* 1. Autofill.
* 2. Sign & Encode.
* 3. Submit.
*
* @param this - A Client.
* @param wallet - A Wallet to sign a transaction.
* @param transaction - A transaction to autofill, sign & encode, and submit.
* @param opts - (Optional) Options used to sign and submit a transaction.
* @param opts.autofill - If true, autofill a transaction.
* @param opts.failHard - If true, and the transaction fails locally, do not retry or relay the transaction to other servers.
* @param opts.wallet - A wallet to sign a transaction. It must be provided when submitting an unsigned transaction.
* @returns A promise that contains SubmitResponse.
* @throws RippledError if submit request fails.
*/
async function submit(
this: Client,
wallet: Wallet,
transaction: Transaction,
transaction: Transaction | string,
opts?: SubmitOptions,
): Promise<SubmitResponse> {
const tx = await this.autofill(transaction)
const { tx_blob } = wallet.sign(tx)
return this.submitSigned(tx_blob)
}
/**
* Encodes and submits a signed transaction.
*
* @param this - A Client.
* @param signedTransaction - A signed transaction to encode (if not already) and submit.
* @returns A promise that contains SubmitResponse.
* @throws ValidationError if the transaction isn't signed, RippledError if submit request fails.
*/
async function submitSigned(
this: Client,
signedTransaction: Transaction | string,
): 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),
}
return this.request(request)
const signedTx = await getSignedTx(this, transaction, opts)
return submitRequest(this, signedTx, opts?.failHard)
}
/**
@@ -72,62 +55,56 @@ async function submitSigned(
* See [Reliable Transaction Submission](https://xrpl.org/reliable-transaction-submission.html).
*
* @param this - A Client.
* @param wallet - A Wallet to sign a transaction.
* @param transaction - A transaction to autofill, sign & encode, and submit.
* @param opts - (Optional) Options used to sign and submit a transaction.
* @param opts.autofill - If true, autofill a transaction.
* @param opts.failHard - If true, and the transaction fails locally, do not retry or relay the transaction to other servers.
* @param opts.wallet - A wallet to sign a transaction. It must be provided when submitting an unsigned transaction.
* @returns A promise that contains TxResponse, that will return when the transaction has been validated.
*/
async function submitReliable(
async function submitAndWait(
this: Client,
wallet: Wallet,
transaction: Transaction,
transaction: Transaction | string,
opts?: SubmitOptions,
): Promise<TxResponse> {
const tx = await this.autofill(transaction)
const { tx_blob } = wallet.sign(tx)
return this.submitSignedReliable(tx_blob)
}
const signedTx = await getSignedTx(this, transaction, opts)
/**
* 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).
* See [Reliable Transaction Submission](https://xrpl.org/reliable-transaction-submission.html).
*
* @param this - A Client.
* @param signedTransaction - A signed transaction to encode (if not already) and submit.
* @returns A promise that contains TxResponse, that will return when the transaction has been validated.
* @throws ValidationError if the request is not signed/doesn't have a LastLedgerSequence, RippledError if the submit request
* fails, XrplError if the reliable submission fails.
*/
async function submitSignedReliable(
this: Client,
signedTransaction: Transaction | string,
): Promise<TxResponse> {
if (!isSigned(signedTransaction)) {
throw new ValidationError('Transaction must be signed')
}
if (!hasLastLedgerSequence(signedTransaction)) {
if (!hasLastLedgerSequence(signedTx)) {
throw new ValidationError(
'Transaction must contain a LastLedgerSequence value for reliable submission.',
)
}
const signedTxEncoded =
typeof signedTransaction === 'string'
? signedTransaction
: encode(signedTransaction)
const txHash = hashes.hashSignedTx(signedTransaction)
const request: SubmitRequest = {
command: 'submit',
tx_blob: signedTxEncoded,
fail_hard: isAccountDelete(signedTransaction),
}
await this.request(request)
await submitRequest(this, signedTx, opts?.failHard)
const txHash = hashes.hashSignedTx(signedTx)
return waitForFinalTransactionOutcome(this, txHash)
}
// Helper functions
// Encodes and submits a signed transaction.
async function submitRequest(
client: Client,
signedTransaction: Transaction | 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)
}
/*
* The core logic of reliable submission. This polls the ledger until the result of the
* transaction can be considered final, meaning it has either been included in a
@@ -172,6 +149,35 @@ function isSigned(transaction: Transaction | string): boolean {
)
}
// initializes a transaction for a submit request
async function getSignedTx(
client: Client,
transaction: Transaction | string,
{ autofill = true, wallet }: SubmitOptions = {},
): Promise<Transaction | 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 Transaction)
: 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
function hasLastLedgerSequence(transaction: Transaction | string): boolean {
const tx = typeof transaction === 'string' ? decode(transaction) : transaction
@@ -184,4 +190,4 @@ function isAccountDelete(transaction: Transaction | string): boolean {
return tx.TransactionType === 'AccountDelete'
}
export { submit, submitSigned, submitReliable, submitSignedReliable }
export { submit, submitAndWait }

109
test/client/submit.ts Normal file
View File

@@ -0,0 +1,109 @@
import { assert } from 'chai'
import _ from 'lodash'
import { ValidationError } from 'xrpl-local'
import { Transaction } from 'xrpl-local/models/transactions'
import Wallet from 'xrpl-local/wallet'
import rippled from '../fixtures/rippled'
import { setupClient, teardownClient } from '../setupClient'
import { assertRejects } from '../testUtils'
describe('client.submit', function () {
beforeEach(setupClient)
afterEach(teardownClient)
describe('submit unsigned transactions', function () {
const publicKey =
'030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D'
const privateKey =
'00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F'
const address = 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc'
const transaction: Transaction = {
TransactionType: 'Payment',
Account: address,
Destination: 'rQ3PTWGLCbPz8ZCicV5tCX3xuymojTng5r',
Amount: '20000000',
Sequence: 1,
Fee: '12',
LastLedgerSequence: 12312,
}
it('should submit an unsigned transaction', async function () {
const tx = _.cloneDeep(transaction)
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.submit(tx, { wallet })
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 without a wallet', async function () {
const tx: Transaction = _.cloneDeep(transaction)
delete tx.SigningPubKey
delete tx.TxnSignature
this.mockRippled.addResponse('submit', rippled.submit.success)
await assertRejects(
this.client.submit(tx),
ValidationError,
'Wallet must be provided when submitting an unsigned transaction',
)
})
})
describe('submit signed transactions', function () {
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 = { ...signedTransaction }
this.mockRippled.addResponse('submit', rippled.submit.success)
try {
const response = await this.client.submit(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.submit(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}`)
}
})
})
})

View File

@@ -1,69 +0,0 @@
import { assert } from 'chai'
import { ValidationError } from 'xrpl-local'
import { Transaction } from 'xrpl-local/models/transactions'
import rippled from '../fixtures/rippled'
import { setupClient, teardownClient } from '../setupClient'
import { assertRejects } from '../testUtils'
describe('client.submitSigned', 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.submitSigned(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.submitSigned(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)
await assertRejects(
this.client.submitSigned(signedTx),
ValidationError,
'Transaction must be signed',
)
})
})

View File

@@ -1,44 +0,0 @@
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.submit', 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.submit(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}`)
}
})
})

View File

@@ -66,7 +66,7 @@ describe('integration tests', function () {
const { tx_blob: tx_blob1 } = signerWallet1.sign(accountSetTx, true)
const { tx_blob: tx_blob2 } = signerWallet2.sign(accountSetTx, true)
const multisignedTx = multisign([tx_blob1, tx_blob2])
const submitResponse = await client.submitSigned(multisignedTx)
const submitResponse = await client.submit(multisignedTx)
await ledgerAccept(client)
assert.strictEqual(submitResponse.result.engine_result, 'tesSUCCESS')
await verifySubmittedTransaction(this.client, multisignedTx)

View File

@@ -47,7 +47,7 @@ async function generateFundedWalletWithRegularKey(
}
// Add a regular key to the first account
await client.submit(masterWallet, setRegularTx)
await client.submit(setRegularTx, { wallet: masterWallet })
if (disableMasterKey) {
const accountSet: AccountSet = {
@@ -112,7 +112,7 @@ describe('regular key', function () {
}
const client: Client = this.client
const response = await client.submit(masterWallet, tx)
const response = await client.submit(tx, { wallet: masterWallet })
assert.equal(
response.result.engine_result,
'tefMASTER_DISABLED',
@@ -151,7 +151,9 @@ describe('regular key', function () {
}
const client: Client = this.client
const response = await client.submit(masterWallet, enableMasterKey)
const response = await client.submit(enableMasterKey, {
wallet: masterWallet,
})
assert.equal(
response.result.engine_result,
'tefMASTER_DISABLED',
@@ -178,7 +180,7 @@ describe('regular key', function () {
},
}
const response2 = await client.submit(regularKeyWallet, tx)
const response2 = await client.submit(tx, { wallet: regularKeyWallet })
assert.equal(
response2.result.engine_result,
'tefBAD_AUTH',
@@ -225,7 +227,7 @@ describe('regular key', function () {
const signed1 = regularKeyWallet.sign(accountSetTx, true)
const signed2 = signerWallet2.sign(accountSetTx, true)
const multisigned = multisign([signed1.tx_blob, signed2.tx_blob])
const submitResponse = await client.submitSigned(multisigned)
const submitResponse = await client.submit(multisigned)
await ledgerAccept(client)
assert.strictEqual(submitResponse.result.engine_result, 'tesSUCCESS')
@@ -276,7 +278,7 @@ describe('regular key', function () {
const signed1 = sameKeyDefaultAddressWallet.sign(accountSetTx, true)
const signed2 = signerWallet2.sign(accountSetTx, true)
const multisigned = multisign([signed1.tx_blob, signed2.tx_blob])
const submitResponse = await client.submitSigned(multisigned)
const submitResponse = await client.submit(multisigned)
await ledgerAccept(client)
assert.strictEqual(submitResponse.result.engine_result, 'tefBAD_SIGNATURE')
})

View File

@@ -118,7 +118,7 @@ describe('subscribe', function () {
// The transactions_proposed stream should trigger the transaction handler WITHOUT ledgerAccept
const client: Client = this.client
client.submit(this.wallet, tx)
client.submit(tx, { wallet: this.wallet })
})
})

View File

@@ -25,10 +25,9 @@ describe('tx', function () {
Domain: convertStringToHex('example.com'),
}
const response: SubmitResponse = await this.client.submit(
this.wallet,
accountSet,
)
const response: SubmitResponse = await this.client.submit(accountSet, {
wallet: this.wallet,
})
const hash = hashSignedTx(response.result.tx_blob)
const txResponse = await this.client.request({

View File

@@ -3,7 +3,9 @@
import { assert } from 'chai'
import _ from 'lodash'
import { AccountSet, convertStringToHex } from 'xrpl-local'
import { AccountSet, convertStringToHex, ValidationError } from 'xrpl-local'
import { assertRejects } from '../testUtils'
import serverUrl from './serverUrl'
import { setupClient, teardownClient } from './setup'
@@ -12,19 +14,21 @@ import { ledgerAccept } from './utils'
// how long before each test case times out
const TIMEOUT = 60000
describe('reliable submission', function () {
describe('client.submitAndWait', function () {
this.timeout(TIMEOUT)
beforeEach(_.partial(setupClient, serverUrl))
afterEach(teardownClient)
it('submitReliable', async function () {
it('submitAndWait an unsigned transaction', async function () {
const accountSet: AccountSet = {
TransactionType: 'AccountSet',
Account: this.wallet.classicAddress,
Domain: convertStringToHex('example.com'),
}
const responsePromise = this.client.submitReliable(this.wallet, accountSet)
const responsePromise = this.client.submitAndWait(accountSet, {
wallet: this.wallet,
})
const ledgerPromise = setTimeout(ledgerAccept, 1000, this.client)
return Promise.all([responsePromise, ledgerPromise]).then(
([response, _ledger]) => {
@@ -34,7 +38,21 @@ describe('reliable submission', function () {
)
})
it('submitSignedReliable', async function () {
it('should throw a ValidationError when submitting an unsigned transaction without a wallet', async function () {
const accountSet: AccountSet = {
TransactionType: 'AccountSet',
Account: this.wallet.classicAddress,
Domain: convertStringToHex('example.com'),
}
await assertRejects(
this.client.submitAndWait(accountSet),
ValidationError,
'Wallet must be provided when submitting an unsigned transaction',
)
})
it('submitAndWait a signed transaction', async function () {
const accountSet: AccountSet = {
TransactionType: 'AccountSet',
Account: this.wallet.classicAddress,
@@ -43,7 +61,7 @@ describe('reliable submission', function () {
const { tx_blob: signedAccountSet } = this.wallet.sign(
await this.client.autofill(accountSet),
)
const responsePromise = this.client.submitSignedReliable(signedAccountSet)
const responsePromise = this.client.submitAndWait(signedAccountSet)
const ledgerPromise = setTimeout(ledgerAccept, 1000, this.client)
return Promise.all([responsePromise, ledgerPromise]).then(
([response, _ledger]) => {
@@ -53,7 +71,7 @@ describe('reliable submission', function () {
)
})
it('submitSignedReliable longer', async function () {
it('submitAndWait a signed transaction longer', async function () {
const accountSet: AccountSet = {
TransactionType: 'AccountSet',
Account: this.wallet.classicAddress,
@@ -62,7 +80,7 @@ describe('reliable submission', function () {
const { tx_blob: signedAccountSet } = this.wallet.sign(
await this.client.autofill(accountSet),
)
const responsePromise = this.client.submitSignedReliable(signedAccountSet)
const responsePromise = this.client.submitAndWait(signedAccountSet)
const ledgerPromise = setTimeout(ledgerAccept, 5000, this.client)
return Promise.all([responsePromise, ledgerPromise]).then(
([response, _ledger]) => {

View File

@@ -28,8 +28,8 @@ describe('PaymentChannelClaim', function () {
}
const paymentChannelResponse = await this.client.submit(
this.wallet,
paymentChannelCreate,
{ wallet: this.wallet },
)
await testTransaction(this.client, paymentChannelCreate, this.wallet)

View File

@@ -28,8 +28,8 @@ describe('PaymentChannelFund', function () {
}
const paymentChannelResponse = await this.client.submit(
this.wallet,
paymentChannelCreate,
{ wallet: this.wallet },
)
await testTransaction(this.client, paymentChannelCreate, this.wallet)

View File

@@ -30,7 +30,9 @@ export async function fundAccount(
// 2 times the amount needed for a new account (20 XRP)
Amount: '400000000',
}
const response = await client.submit(Wallet.fromSeed(masterSecret), payment)
const response = await client.submit(payment, {
wallet: Wallet.fromSeed(masterSecret),
})
if (response.result.engine_result !== 'tesSUCCESS') {
// eslint-disable-next-line no-console -- happens only when something goes wrong
console.log(response)
@@ -86,7 +88,7 @@ export async function testTransaction(
await ledgerAccept(client)
// sign/submit the transaction
const response = await client.submit(wallet, transaction)
const response = await client.submit(transaction, { wallet })
// check that the transaction was successful
assert.equal(response.type, 'response')