From 138e7942daf7e709c2b83dfb74a04e209af2b2a1 Mon Sep 17 00:00:00 2001 From: Elliot Lee Date: Mon, 6 Jan 2020 04:01:10 -0800 Subject: [PATCH] Add support for AccountDelete (#1120) https://xrpl.org/accountdelete.html --- docs/index.md | 2 +- docs/src/prepareTransaction.md.ejs | 2 +- src/common/schema-validator.ts | 2 + .../schemas/objects/transaction-type.json | 4 +- .../schemas/output/get-transaction.json | 24 +++++++ .../specifications/account-delete.json | 29 ++++++++ .../specifications/deposit-preauth.json | 21 ++++++ src/ledger/parse/account-delete.ts | 34 ++++++++++ src/ledger/parse/transaction.ts | 54 ++++++++------- test/api/getTransaction/index.ts | 10 +++ test/api/prepareTransaction/index.ts | 27 ++++++++ test/api/sign/index.ts | 6 +- .../get-transaction-account-delete.json | 32 +++++++++ test/fixtures/responses/index.js | 3 +- test/fixtures/rippled/index.js | 3 +- test/fixtures/rippled/tx/account-delete.json | 66 +++++++++++++++++++ test/mock-rippled.ts | 5 ++ test/ripple-api-test.ts | 2 +- test/utils.ts | 2 +- 19 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 src/common/schemas/specifications/account-delete.json create mode 100644 src/common/schemas/specifications/deposit-preauth.json create mode 100644 src/ledger/parse/account-delete.ts create mode 100644 test/fixtures/responses/get-transaction-account-delete.json create mode 100644 test/fixtures/rippled/tx/account-delete.json diff --git a/docs/index.md b/docs/index.md index 48e159bb..60842a1d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4511,7 +4511,7 @@ Prepare a transaction. The prepared transaction must subsequently be [signed](#s This method works with any of [the transaction types supported by rippled](https://developers.ripple.com/transaction-types.html). -Notably, this is the preferred method for preparing a `DepositPreauth` transaction (added in rippled 1.1.0). +Notably, this is the preferred method for preparing `DepositPreauth` or `AccountDelete` transactions. ### Parameters diff --git a/docs/src/prepareTransaction.md.ejs b/docs/src/prepareTransaction.md.ejs index 6f5c0c54..48f4bdd9 100644 --- a/docs/src/prepareTransaction.md.ejs +++ b/docs/src/prepareTransaction.md.ejs @@ -6,7 +6,7 @@ Prepare a transaction. The prepared transaction must subsequently be [signed](#s This method works with any of [the transaction types supported by rippled](https://developers.ripple.com/transaction-types.html). -Notably, this is the preferred method for preparing a `DepositPreauth` transaction (added in rippled 1.1.0). +Notably, this is the preferred method for preparing `DepositPreauth` or `AccountDelete` transactions. ### Parameters diff --git a/src/common/schema-validator.ts b/src/common/schema-validator.ts index a1340c3a..20a6a93b 100644 --- a/src/common/schema-validator.ts +++ b/src/common/schema-validator.ts @@ -62,6 +62,8 @@ function loadSchemas() { require('./schemas/specifications/check-cash.json'), require('./schemas/specifications/check-cancel.json'), require('./schemas/specifications/trustline.json'), + require('./schemas/specifications/deposit-preauth.json'), + require('./schemas/specifications/account-delete.json'), require('./schemas/output/sign.json'), require('./schemas/output/submit.json'), require('./schemas/output/get-account-info.json'), diff --git a/src/common/schemas/objects/transaction-type.json b/src/common/schemas/objects/transaction-type.json index 0b120cf9..5d8433e7 100644 --- a/src/common/schemas/objects/transaction-type.json +++ b/src/common/schemas/objects/transaction-type.json @@ -18,6 +18,8 @@ "paymentChannelClaim", "checkCreate", "checkCancel", - "checkCash" + "checkCash", + "depositPreauth", + "accountDelete" ] } diff --git a/src/common/schemas/output/get-transaction.json b/src/common/schemas/output/get-transaction.json index dcf86139..150b3558 100644 --- a/src/common/schemas/output/get-transaction.json +++ b/src/common/schemas/output/get-transaction.json @@ -208,6 +208,30 @@ "$ref": "paymentChannelClaim" } } + }, + { + "properties": { + "type": { + "enum": [ + "depositPreauth" + ] + }, + "specification": { + "$ref": "depositPreauth" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "accountDelete" + ] + }, + "specification": { + "$ref": "accountDelete" + } + } } ] } diff --git a/src/common/schemas/specifications/account-delete.json b/src/common/schemas/specifications/account-delete.json new file mode 100644 index 00000000..2fa8c05d --- /dev/null +++ b/src/common/schemas/specifications/account-delete.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "accountDelete", + "link": "account-delete", + "type": "object", + "properties": { + "destination": { + "$ref": "address", + "description": "Address of an account to receive any leftover XRP after deleting the sending account. Must be a funded account in the ledger, and must not be the sending account." + }, + "destinationTag": { + "$ref": "tag", + "description": "(Optional) Arbitrary destination tag that identifies a hosted recipient or other information for the recipient of the deleted account's leftover XRP." + }, + "destinationXAddress": { + "$ref": "address", + "description": "X-address of an account to receive any leftover XRP after deleting the sending account. Must be a funded account in the ledger, and must not be the sending account." + } + }, + "anyOf": [ + { + "required": ["destination"] + }, + { + "required": ["destinationXAddress"] + } + ], + "additionalProperties": false +} diff --git a/src/common/schemas/specifications/deposit-preauth.json b/src/common/schemas/specifications/deposit-preauth.json new file mode 100644 index 00000000..02c826d9 --- /dev/null +++ b/src/common/schemas/specifications/deposit-preauth.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "depositPreauth", + "link": "deposit-preauth", + "type": "object", + "properties": { + "authorize": { + "$ref": "address", + "description": "Address of the account that can cash the check." + }, + "unauthorize": { + "$ref": "address", + "description": "Address of the account that can cash the check." + } + }, + "oneOf": [ + {"required": ["authorize"]}, + {"required": ["unauthorize"]} + ], + "additionalProperties": false +} diff --git a/src/ledger/parse/account-delete.ts b/src/ledger/parse/account-delete.ts new file mode 100644 index 00000000..6f45d6bb --- /dev/null +++ b/src/ledger/parse/account-delete.ts @@ -0,0 +1,34 @@ +import * as assert from 'assert' +import {removeUndefined} from '../../common' +import {classicAddressToXAddress} from 'ripple-address-codec' + +export type FormattedAccountDelete = { + // account (address) of an account to receive any leftover XRP after deleting the sending account. + // Must be a funded account in the ledger, and must not be the sending account. + destination: string + + // (Optional) Arbitrary destination tag that identifies a hosted recipient or other information + // for the recipient of the deleted account's leftover XRP. NB: Ensure that the hosted recipient is + // able to account for AccountDelete transactions; if not, your balance may not be properly credited. + destinationTag?: number + + // X-address of an account to receive any leftover XRP after deleting the sending account. + // Must be a funded account in the ledger, and must not be the sending account. + destinationXAddress: string +} + +function parseAccountDelete(tx: any): FormattedAccountDelete { + assert.ok(tx.TransactionType === 'AccountDelete') + + return removeUndefined({ + destination: tx.Destination, + destinationTag: tx.DestinationTag, + destinationXAddress: classicAddressToXAddress( + tx.Destination, + tx.DestinationTag === undefined ? false : tx.DestinationTag, + false + ) + }) +} + +export default parseAccountDelete diff --git a/src/ledger/parse/transaction.ts b/src/ledger/parse/transaction.ts index a6290faf..6764d2b0 100644 --- a/src/ledger/parse/transaction.ts +++ b/src/ledger/parse/transaction.ts @@ -1,27 +1,31 @@ import {parseOutcome} from './utils' import {removeUndefined} from '../../common' -import parsePayment from './payment' -import parseTrustline from './trustline' -import parseOrder from './order' -import parseOrderCancellation from './cancellation' + import parseSettings from './settings' +import parseAccountDelete from './account-delete' +import parseCheckCancel from './check-cancel' +import parseCheckCash from './check-cash' +import parseCheckCreate from './check-create' +import parseDepositPreauth from './deposit-preauth' +import parseEscrowCancellation from './escrow-cancellation' import parseEscrowCreation from './escrow-creation' import parseEscrowExecution from './escrow-execution' -import parseEscrowCancellation from './escrow-cancellation' -import parseCheckCreate from './check-create' -import parseCheckCash from './check-cash' -import parseCheckCancel from './check-cancel' -import parseDepositPreauth from './deposit-preauth' +import parseOrderCancellation from './cancellation' +import parseOrder from './order' +import parsePayment from './payment' +import parsePaymentChannelClaim from './payment-channel-claim' import parsePaymentChannelCreate from './payment-channel-create' import parsePaymentChannelFund from './payment-channel-fund' -import parsePaymentChannelClaim from './payment-channel-claim' -import parseFeeUpdate from './fee-update' -import parseAmendment from './amendment' +import parseTrustline from './trustline' + +import parseAmendment from './amendment' // pseudo-transaction +import parseFeeUpdate from './fee-update' // pseudo-transaction function parseTransactionType(type) { // Ordering matches https://developers.ripple.com/transaction-types.html const mapping = { AccountSet: 'settings', + AccountDelete: 'accountDelete', CheckCancel: 'checkCancel', CheckCash: 'checkCash', CheckCreate: 'checkCreate', @@ -49,23 +53,25 @@ function parseTransactionType(type) { function parseTransaction(tx: any, includeRawTransaction: boolean): any { const type = parseTransactionType(tx.TransactionType) const mapping = { - payment: parsePayment, - trustline: parseTrustline, - order: parseOrder, - orderCancellation: parseOrderCancellation, settings: parseSettings, + accountDelete: parseAccountDelete, + checkCancel: parseCheckCancel, + checkCash: parseCheckCash, + checkCreate: parseCheckCreate, + depositPreauth: parseDepositPreauth, + escrowCancellation: parseEscrowCancellation, escrowCreation: parseEscrowCreation, escrowExecution: parseEscrowExecution, - escrowCancellation: parseEscrowCancellation, - checkCreate: parseCheckCreate, - checkCash: parseCheckCash, - checkCancel: parseCheckCancel, - depositPreauth: parseDepositPreauth, + orderCancellation: parseOrderCancellation, + order: parseOrder, + payment: parsePayment, + paymentChannelClaim: parsePaymentChannelClaim, paymentChannelCreate: parsePaymentChannelCreate, paymentChannelFund: parsePaymentChannelFund, - paymentChannelClaim: parsePaymentChannelClaim, - feeUpdate: parseFeeUpdate, - amendment: parseAmendment + trustline: parseTrustline, + + amendment: parseAmendment, // pseudo-transaction + feeUpdate: parseFeeUpdate // pseudo-transaction } const parser: Function = mapping[type] diff --git a/test/api/getTransaction/index.ts b/test/api/getTransaction/index.ts index 446d1fd7..4487996e 100644 --- a/test/api/getTransaction/index.ts +++ b/test/api/getTransaction/index.ts @@ -354,6 +354,16 @@ export default { ) }, + 'AccountDelete': async (api, address) => { + const hash = 'EC2AB14028DC84DE525470AB4DAAA46358B50A8662C63804BFF38244731C0CB9' + const response = await api.getTransaction(hash) + assertResultMatch( + response, + RESPONSE_FIXTURES.accountDelete, + 'getTransaction' + ) + }, + 'no Meta': async (api, address) => { const hash = 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA1B' diff --git a/test/api/prepareTransaction/index.ts b/test/api/prepareTransaction/index.ts index 575b0a60..9ebf2029 100644 --- a/test/api/prepareTransaction/index.ts +++ b/test/api/prepareTransaction/index.ts @@ -745,6 +745,33 @@ export default { return assertResultMatch(response, expected, 'prepare') }, + 'AccountDelete': async (api, address) => { + const localInstructions = { + ...instructionsWithMaxLedgerVersionOffset, + maxFee: '5.0' // 5 XRP fee for AccountDelete + } + + const txJSON = { + TransactionType: 'AccountDelete', + Account: address, + Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe' + } + + const response = await api.prepareTransaction(txJSON, localInstructions) + const expected = { + txJSON: + '{"TransactionType":"AccountDelete","Account":"' + + address + + '","Destination":"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe","Flags":2147483648,"LastLedgerSequence":8820051,"Fee":"12","Sequence":23}', + instructions: { + fee: '0.000012', + sequence: 23, + maxLedgerVersion: 8820051 + } + } + return assertResultMatch(response, expected, 'prepare') + }, + // prepareTransaction - Payment 'Payment - normal': async (api, address) => { const localInstructions = { diff --git a/test/api/sign/index.ts b/test/api/sign/index.ts index ae9a2090..46f083ea 100644 --- a/test/api/sign/index.ts +++ b/test/api/sign/index.ts @@ -212,7 +212,7 @@ export default { ) => { const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' const request = { - // TODO: This fails when address is X-Address + // TODO: This fails when address is X-address txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1.2","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, instructions: { fee: '0.0000012', @@ -232,7 +232,7 @@ export default { ) => { const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' const request = { - // TODO: This fails when address is X-Address + // TODO: This fails when address is X-address txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"1123456.7","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, instructions: { fee: '1.1234567', @@ -289,7 +289,7 @@ export default { api._maxFeeXRP = '2.1' const secret = 'shsWGZcmZz6YsWWmcnpfr6fLTdtFV' const request = { - // TODO: This fails when address is X-Address + // TODO: This fails when address is X-address txJSON: `{"Flags":2147483648,"TransactionType":"AccountSet","Account":"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59","Domain":"726970706C652E636F6D","LastLedgerSequence":8820051,"Fee":"2010000","Sequence":23,"SigningPubKey":"02F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D8"}`, instructions: { fee: '2.01', diff --git a/test/fixtures/responses/get-transaction-account-delete.json b/test/fixtures/responses/get-transaction-account-delete.json new file mode 100644 index 00000000..45ca8f6d --- /dev/null +++ b/test/fixtures/responses/get-transaction-account-delete.json @@ -0,0 +1,32 @@ +{ + "type": "accountDelete", + "address": "rM5qup5BYDLMXaR5KU1hiC9HhFMuBVrnKv", + "sequence": 3227049, + "id": "EC2AB14028DC84DE525470AB4DAAA46358B50A8662C63804BFF38244731C0CB9", + "specification": { + "destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + "destinationXAddress": "XV5kHfQmzDQjbFNv4jX3FX9Y7ig5QhpKGEFCq4mdLfhdxMq" + }, + "outcome": { + "result": "tesSUCCESS", + "timestamp": "2019-12-17T09:16:51.000Z", + "fee": "5", + "balanceChanges": { + "rM5qup5BYDLMXaR5KU1hiC9HhFMuBVrnKv": [ + { + "currency": "XRP", + "value": "-10000" + } + ], + "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe": [ + { + "currency": "XRP", + "value": "9995" + } + ] + }, + "orderbookChanges": {}, + "ledgerVersion": 3232071, + "indexInLedger": 0 + } +} diff --git a/test/fixtures/responses/index.js b/test/fixtures/responses/index.js index ba09cb18..5c4a0bb5 100644 --- a/test/fixtures/responses/index.js +++ b/test/fixtures/responses/index.js @@ -67,7 +67,8 @@ module.exports = { paymentChannelClaim: require('./get-transaction-payment-channel-claim.json'), amendment: require('./get-transaction-amendment.json'), - feeUpdate: require('./get-transaction-fee-update.json') + feeUpdate: require('./get-transaction-fee-update.json'), + accountDelete: require('./get-transaction-account-delete.json') }, getTransactions: { normal: require('./get-transactions.json'), diff --git a/test/fixtures/rippled/index.js b/test/fixtures/rippled/index.js index 40ec6440..f3dd44e3 100644 --- a/test/fixtures/rippled/index.js +++ b/test/fixtures/rippled/index.js @@ -99,6 +99,7 @@ module.exports = { NoMeta: require('./tx/no-meta.json'), LedgerZero: require('./tx/ledger-zero.json'), Amendment: require('./tx/amendment.json'), - SetFee: require('./tx/set-fee.json') + SetFee: require('./tx/set-fee.json'), + AccountDelete: require('./tx/account-delete.json') } }; diff --git a/test/fixtures/rippled/tx/account-delete.json b/test/fixtures/rippled/tx/account-delete.json new file mode 100644 index 00000000..628b56f0 --- /dev/null +++ b/test/fixtures/rippled/tx/account-delete.json @@ -0,0 +1,66 @@ +{ + "id": 0, + "result": { + "Account": "rM5qup5BYDLMXaR5KU1hiC9HhFMuBVrnKv", + "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + "Fee": "5000000", + "Flags": 2147483648, + "LastLedgerSequence": 3232818, + "Sequence": 3227049, + "SigningPubKey": "022E0DBF14BC4CFF96BC839557EE6F12F6DA45DCD917376F805E65D1B1C60A8CE6", + "TransactionType": "AccountDelete", + "TxnSignature": "304402207BDBE1B71C8BD00363905817C9373880CE9E8F0080623D457495E2B760BBBEE402202EDEB977D1ED865C1EAB88FE28581E3F8A672097B8BB0956E977C6EC87CA668C", + "date": 629889411, + "hash": "EC2AB14028DC84DE525470AB4DAAA46358B50A8662C63804BFF38244731C0CB9", + "inLedger": 3232071, + "ledger_index": 3232071, + "meta": { + "AffectedNodes": [ + { + "DeletedNode": { + "FinalFields": { + "Account": "rM5qup5BYDLMXaR5KU1hiC9HhFMuBVrnKv", + "Balance": "0", + "Flags": 0, + "OwnerCount": 0, + "PreviousTxnID": "2737BEDDDA1D7FB523CFB84B891216331CC4CC999349828D81C6C727A1115A44", + "PreviousTxnLgrSeq": 3227049, + "Sequence": 3227050 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "08EBF94D7BB527442E0B51F533B902DDCFBD423D8E95FAE752BE7876A29E875B", + "PreviousFields": { + "Balance": "10000000000", + "Sequence": 3227049 + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + "Balance": "99991026734967448", + "Flags": 0, + "OwnerCount": 0, + "Sequence": 2978 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "31CCE9D28412FF973E9AB6D0FA219BACF19687D9A2456A0C2ABC3280E9D47E37", + "PreviousFields": { + "Balance": "99991016739967448" + }, + "PreviousTxnID": "2737BEDDDA1D7FB523CFB84B891216331CC4CC999349828D81C6C727A1115A44", + "PreviousTxnLgrSeq": 3227049 + } + } + ], + "DeliveredAmount": "9995000000", + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "delivered_amount": "9995000000" + }, + "validated": true + }, + "status": "success", + "type": "response" +} \ No newline at end of file diff --git a/test/mock-rippled.ts b/test/mock-rippled.ts index 2c6b7a33..95ee24af 100644 --- a/test/mock-rippled.ts +++ b/test/mock-rippled.ts @@ -524,6 +524,11 @@ export function createMockRippled(port) { '81B9ECAE7195EB6E8034AEDF44D8415A7A803E14513FDBB34FA984AB37D59563' ) { conn.send(createResponse(request, fixtures.tx.PaymentChannelClaim)) + } else if ( + request.transaction === + 'EC2AB14028DC84DE525470AB4DAAA46358B50A8662C63804BFF38244731C0CB9' + ) { + conn.send(createResponse(request, fixtures.tx.AccountDelete)) } else if ( request.transaction === 'AFB3ADF22F3C605E23FAEFAA185F3BD763C4692CAC490D9819D117CD33BFAA11' diff --git a/test/ripple-api-test.ts b/test/ripple-api-test.ts index 73c2969b..ce77c1a3 100644 --- a/test/ripple-api-test.ts +++ b/test/ripple-api-test.ts @@ -42,7 +42,7 @@ describe('RippleAPI [Test Runner]', function() { }) // Run each test with the newer, x-address style. if (!config.skipXAddress) { - describe(`[X-Address]`, () => { + describe(`[X-address]`, () => { for (const [testName, fn] of tests) { it(testName, function() { return fn(this.api, addresses.ACCOUNT_X) diff --git a/test/utils.ts b/test/utils.ts index 30ea040f..de148f37 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -33,7 +33,7 @@ interface LoadedTestSuite { name: string tests: [string, TestFn][] config: { - /** Set to true to skip re-running tests with an X-Address. */ + /** Set to true to skip re-running tests with an X-address. */ skipXAddress?: boolean } }