feat: extra protection for AccountDelete transactions (#1626)

* add deletion blockers check to autofill

* add tests

* add fail_hard: true

* pass in account_objects response to error

* only fail_hard for AccountDelete

* reject promise instead of throwing error

* fix rebase issue
This commit is contained in:
Mayukha Vadari
2021-09-24 15:05:38 -04:00
parent bfeb737ad7
commit b7c4b16a8d
5 changed files with 92 additions and 49 deletions

View File

@@ -2,8 +2,12 @@ import BigNumber from 'bignumber.js'
import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec' import { xAddressToClassicAddress, isValidXAddress } from 'ripple-address-codec'
import type { Client } from '..' import type { Client } from '..'
import { ValidationError } from '../errors' import { ValidationError, XrplError } from '../errors'
import { AccountInfoRequest, LedgerRequest } from '../models/methods' import {
AccountInfoRequest,
AccountObjectsRequest,
LedgerRequest,
} from '../models/methods'
import { Transaction } from '../models/transactions' import { Transaction } from '../models/transactions'
import setTransactionFlagsToNumber from '../models/utils/flags' import setTransactionFlagsToNumber from '../models/utils/flags'
import { xrpToDrops } from '../utils' import { xrpToDrops } from '../utils'
@@ -46,6 +50,9 @@ async function autofill<T extends Transaction>(
if (tx.LastLedgerSequence == null) { if (tx.LastLedgerSequence == null) {
promises.push(setLatestValidatedLedgerSequence(this, tx)) promises.push(setLatestValidatedLedgerSequence(this, tx))
} }
if (tx.TransactionType === 'AccountDelete') {
promises.push(checkAccountDeleteBlockers(this, tx))
}
return Promise.all(promises).then(() => tx) return Promise.all(promises).then(() => tx)
} }
@@ -193,4 +200,28 @@ async function setLatestValidatedLedgerSequence(
tx.LastLedgerSequence = ledgerSequence + LEDGER_OFFSET tx.LastLedgerSequence = ledgerSequence + LEDGER_OFFSET
} }
async function checkAccountDeleteBlockers(
client: Client,
tx: Transaction,
): Promise<void> {
const request: AccountObjectsRequest = {
command: 'account_objects',
account: tx.Account,
ledger_index: 'validated',
deletion_blockers_only: true,
}
const response = await client.request(request)
return new Promise((resolve, reject) => {
if (response.result.account_objects.length > 0) {
reject(
new XrplError(
`Account ${tx.Account} cannot be deleted; there are Escrows, PayChannels, RippleStates, or Checks associated with the account.`,
response.result.account_objects,
),
)
}
resolve()
})
}
export default autofill export default autofill

View File

@@ -51,6 +51,7 @@ async function submitSignedTransaction(
const request: SubmitRequest = { const request: SubmitRequest = {
command: 'submit', command: 'submit',
tx_blob: signedTxEncoded, tx_blob: signedTxEncoded,
fail_hard: isAccountDelete(signedTransaction),
} }
return this.request(request) return this.request(request)
} }
@@ -63,4 +64,9 @@ function isSigned(transaction: Transaction | string): boolean {
) )
} }
function isAccountDelete(transaction: Transaction | string): boolean {
const tx = typeof transaction === 'string' ? decode(transaction) : transaction
return tx.TransactionType === 'AccountDelete'
}
export { submitTransaction, submitSignedTransaction } export { submitTransaction, submitSignedTransaction }

View File

@@ -1,9 +1,16 @@
import { assert } from 'chai' import { assert } from 'chai'
import { AccountDelete, EscrowFinish, Payment, Transaction } from 'xrpl-local' import {
XrplError,
AccountDelete,
EscrowFinish,
Payment,
Transaction,
} from 'xrpl-local'
import rippled from '../fixtures/rippled' import rippled from '../fixtures/rippled'
import { setupClient, teardownClient } from '../setupClient' import { setupClient, teardownClient } from '../setupClient'
import { assertRejects } from '../testUtils'
const Fee = '10' const Fee = '10'
const Sequence = 1432 const Sequence = 1432
@@ -71,6 +78,27 @@ describe('client.autofill', function () {
assert.strictEqual(txResult.Sequence, 23) assert.strictEqual(txResult.Sequence, 23)
}) })
it('should throw error if account deletion blockers exist', async function () {
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(
'account_objects',
rippled.account_objects.normal,
)
const tx: AccountDelete = {
Account: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn',
TransactionType: 'AccountDelete',
Destination: 'X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqZ',
Fee,
Sequence,
LastLedgerSequence,
}
await assertRejects(this.client.autofill(tx), XrplError)
})
describe('when autofill Fee is missing', function () { describe('when autofill Fee is missing', function () {
it('should autofill Fee of a Transaction', async function () { it('should autofill Fee of a Transaction', async function () {
const tx: Transaction = { const tx: Transaction = {
@@ -80,17 +108,7 @@ describe('client.autofill', function () {
Sequence, Sequence,
LastLedgerSequence, LastLedgerSequence,
} }
this.mockRippled.addResponse('server_info', { this.mockRippled.addResponse('server_info', rippled.server_info.normal)
status: 'success',
type: 'response',
result: {
info: {
validated_ledger: {
base_fee_xrp: 0.00001,
},
},
},
})
const txResult = await this.client.autofill(tx) const txResult = await this.client.autofill(tx)
assert.strictEqual(txResult.Fee, '12') assert.strictEqual(txResult.Fee, '12')
@@ -108,19 +126,9 @@ describe('client.autofill', function () {
} }
this.mockRippled.addResponse('account_info', rippled.account_info.normal) this.mockRippled.addResponse('account_info', rippled.account_info.normal)
this.mockRippled.addResponse('ledger', rippled.ledger.normal) this.mockRippled.addResponse('ledger', rippled.ledger.normal)
this.mockRippled.addResponse('server_info', { this.mockRippled.addResponse('server_info', rippled.server_info.normal)
status: 'success',
type: 'response',
result: {
info: {
validated_ledger: {
base_fee_xrp: 0.00001,
},
},
},
})
const txResult = await this.client.autofill(tx)
const txResult = await this.client.autofill(tx)
assert.strictEqual(txResult.Fee, '399') assert.strictEqual(txResult.Fee, '399')
}) })
@@ -132,17 +140,11 @@ describe('client.autofill', function () {
} }
this.mockRippled.addResponse('account_info', rippled.account_info.normal) this.mockRippled.addResponse('account_info', rippled.account_info.normal)
this.mockRippled.addResponse('ledger', rippled.ledger.normal) this.mockRippled.addResponse('ledger', rippled.ledger.normal)
this.mockRippled.addResponse('server_info', { this.mockRippled.addResponse('server_info', rippled.server_info.normal)
status: 'success', this.mockRippled.addResponse(
type: 'response', 'account_objects',
result: { rippled.account_objects.empty,
info: { )
validated_ledger: {
base_fee_xrp: 0.00001,
},
},
},
})
const txResult = await this.client.autofill(tx) const txResult = await this.client.autofill(tx)
assert.strictEqual(txResult.Fee, '5000000') assert.strictEqual(txResult.Fee, '5000000')
@@ -160,17 +162,7 @@ describe('client.autofill', function () {
} }
this.mockRippled.addResponse('account_info', rippled.account_info.normal) this.mockRippled.addResponse('account_info', rippled.account_info.normal)
this.mockRippled.addResponse('ledger', rippled.ledger.normal) this.mockRippled.addResponse('ledger', rippled.ledger.normal)
this.mockRippled.addResponse('server_info', { this.mockRippled.addResponse('server_info', rippled.server_info.normal)
status: 'success',
type: 'response',
result: {
info: {
validated_ledger: {
base_fee_xrp: 0.00001,
},
},
},
})
const txResult = await this.client.autofill(tx, 4) const txResult = await this.client.autofill(tx, 4)
assert.strictEqual(txResult.Fee, '459') assert.strictEqual(txResult.Fee, '459')

View File

@@ -0,0 +1,13 @@
{
"id": 1,
"status": "success",
"type": "response",
"result": {
"account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
"account_objects": [],
"ledger_hash":
"053DF17D2289D1C4971C22F235BC1FCA7D4B3AE966F842E5819D0749E0B8ECD3",
"ledger_index": 14378733,
"validated": true
}
}

View File

@@ -1,5 +1,6 @@
import normalAccountInfo from './accountInfo.json' import normalAccountInfo from './accountInfo.json'
import notfoundAccountInfo from './accountInfoNotFound.json' import notfoundAccountInfo from './accountInfoNotFound.json'
import emptyAccountObjects from './accountObjectsEmpty.json'
import normalAccountObjects from './accountObjectsNormal.json' import normalAccountObjects from './accountObjectsNormal.json'
import account_offers from './accountOffers' import account_offers from './accountOffers'
import normalAccountTx from './accountTx' import normalAccountTx from './accountTx'
@@ -133,7 +134,7 @@ const streams = {
const account_objects = { const account_objects = {
normal: normalAccountObjects, normal: normalAccountObjects,
// notfound: notfoundAccountObjects empty: emptyAccountObjects,
} }
const account_info = { const account_info = {