Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions modules/sdk-coin-xrp/src/lib/accountDeleteBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { AccountDelete } from 'xrpl';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import utils from './utils';

/**
* Builder for XRPL AccountDelete transactions.
*
* An AccountDelete transaction removes an XRP Ledger account and any objects
* it owns, sending the full remaining balance (including the base reserve) to
* the specified destination address. The protocol requires a minimum fee equal
* to the owner-reserve increment (currently 2 XRP / 2 000 000 drops) and the
* account sequence number must satisfy: Sequence + 256 <= current ledger.
*/
export class AccountDeleteBuilder extends TransactionBuilder {
private _destination: string;
private _destinationTag?: number;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

protected get transactionType(): TransactionType {
return TransactionType.AccountDelete;
}

initBuilder(tx: Transaction): void {
super.initBuilder(tx);

const { destination, destinationTag } = tx.toJson();
if (!destination) {
throw new BuildTransactionError('Missing destination');
}

const normalizeAddress = utils.normalizeAddress({ address: destination, destinationTag });
this.to(normalizeAddress);
}

/**
* Set the destination address (and optional destination tag).
* @param address - bech32 XRP address, optionally with ?dt=<tag> query string
*/
to(address: string): AccountDeleteBuilder {
const { address: xrpAddress, destinationTag } = utils.getAddressDetails(address);
this._destination = xrpAddress;
this._destinationTag = destinationTag;
return this;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
if (!this._sender) {
throw new BuildTransactionError('Sender must be set before building the transaction');
}
if (!this._destination) {
throw new BuildTransactionError('Destination must be set before building the transaction');
}

const accountDeleteFields: AccountDelete = {
TransactionType: 'AccountDelete',
Account: this._sender,
Destination: this._destination,
};

if (typeof this._destinationTag === 'number') {
accountDeleteFields.DestinationTag = this._destinationTag;
}

this._specificFields = accountDeleteFields;

return await super.buildImplementation();
}
}
10 changes: 7 additions & 3 deletions modules/sdk-coin-xrp/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import {
VerifyAddressOptions as BaseVerifyAddressOptions,
TransactionPrebuild,
} from '@bitgo/sdk-core';
import { AccountSet, Amount, Payment, Signer, SignerEntry, SignerListSet, TrustSet } from 'xrpl';
import { AccountDelete, AccountSet, Amount, Payment, Signer, SignerEntry, SignerListSet, TrustSet } from 'xrpl';

export enum XrpTransactionType {
AccountDelete = 'AccountDelete',
AccountSet = 'AccountSet',
Payment = 'Payment',
SignerListSet = 'SignerListSet',
TrustSet = 'TrustSet',
}

export type XrpTransaction = Payment | AccountSet | SignerListSet | TrustSet;
export type XrpTransaction = AccountDelete | Payment | AccountSet | SignerListSet | TrustSet;

export interface Address {
address: string;
Expand Down Expand Up @@ -69,6 +70,9 @@ export interface RecoveryOptions {
krsProvider?: string;
issuerAddress?: string;
currencyCode?: string;
/** When true, builds an AccountDelete transaction to withdraw the full balance
* including the base reserve (currently 10 XRP) instead of a normal Payment. */
reserveWithdrawal?: boolean;
}

export interface HalfSignedTransaction {
Expand Down Expand Up @@ -124,7 +128,7 @@ export interface TxData {
signingPubKey?: string; // if '' then it is a multi sig
txnSignature?: string; // only for single sig
signers?: Signer[]; // only for multi sig
// transfer xrp fields
// transfer xrp / account-delete fields
destination?: string;
destinationTag?: number;
amount?: Amount;
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-xrp/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Utils from './utils';

export { AccountDeleteBuilder } from './accountDeleteBuilder';
export { AccountSetBuilder } from './accountSetBuilder';
export * from './constants';
export * from './iface';
Expand Down
49 changes: 48 additions & 1 deletion modules/sdk-coin-xrp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export class Transaction extends BaseTransaction {
}

switch (this._xrpTransaction.TransactionType) {
case XrpTransactionType.AccountDelete:
txData.destination = this._xrpTransaction.Destination;
txData.destinationTag = this._xrpTransaction.DestinationTag;
return txData;

case XrpTransactionType.Payment:
txData.destination = this._xrpTransaction.Destination;
txData.destinationTag = this._xrpTransaction.DestinationTag;
Expand Down Expand Up @@ -179,6 +184,8 @@ export class Transaction extends BaseTransaction {

explainTransaction(): TransactionExplanation {
switch (this._xrpTransaction.TransactionType) {
case XrpTransactionType.AccountDelete:
return this.explainAccountDeleteTransaction();
case XrpTransactionType.Payment:
return this.explainPaymentTransaction();
case XrpTransactionType.AccountSet:
Expand All @@ -190,6 +197,29 @@ export class Transaction extends BaseTransaction {
}
}

private explainAccountDeleteTransaction(): BaseTransactionExplanation {
const tx = this._xrpTransaction as xrpl.AccountDelete;
const address = utils.normalizeAddress({ address: tx.Destination, destinationTag: tx.DestinationTag });

return {
displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee'],
id: this._id as string,
changeOutputs: [],
outputAmount: '0', // full balance is swept; exact amount unknown at build time
changeAmount: 0,
outputs: [
{
address,
amount: '0',
},
],
fee: {
fee: tx.Fee as string,
feeRate: undefined,
},
};
}

private explainPaymentTransaction(): BaseTransactionExplanation {
const tx = this._xrpTransaction as xrpl.Payment;
const address = utils.normalizeAddress({ address: tx.Destination, destinationTag: tx.DestinationTag });
Expand Down Expand Up @@ -312,6 +342,9 @@ export class Transaction extends BaseTransaction {
this._id = this.calculateIdFromRawTx(txHex);

switch (this._xrpTransaction.TransactionType) {
case XrpTransactionType.AccountDelete:
this.setTransactionType(TransactionType.AccountDelete);
break;
case XrpTransactionType.SignerListSet:
this.setTransactionType(TransactionType.WalletInitialization);
break;
Expand Down Expand Up @@ -339,6 +372,21 @@ export class Transaction extends BaseTransaction {
if (!this._xrpTransaction) {
return;
}
const coin = this._coinConfig.name;

if (this._xrpTransaction.TransactionType === XrpTransactionType.AccountDelete) {
const { Account, Destination, DestinationTag } = this._xrpTransaction;
// For AccountDelete the exact amount swept is unknown at build time (full balance minus fee).
// We record '0' as a placeholder; the actual amount is determined on-chain.
this.inputs.push({ address: Account, value: '0', coin });
this.outputs.push({
address: utils.normalizeAddress({ address: Destination, destinationTag: DestinationTag }),
value: '0',
coin,
});
return;
}

if (this._xrpTransaction.TransactionType === XrpTransactionType.Payment) {
let value: string;
const { Account, Destination, Amount, DestinationTag } = this._xrpTransaction;
Expand All @@ -347,7 +395,6 @@ export class Transaction extends BaseTransaction {
} else {
value = Amount.value;
}
const coin = this._coinConfig.name;
this.inputs.push({
address: Account,
value,
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-coin-xrp/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import xrpl from 'xrpl';
import { AccountDeleteBuilder } from './accountDeleteBuilder';
import { AccountSetBuilder } from './accountSetBuilder';
import { TokenTransferBuilder } from './tokenTransferBuilder';
import { Transaction } from './transaction';
Expand Down Expand Up @@ -28,6 +29,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const tx = this.parseTransaction(txHex);
try {
switch (tx.type) {
case TransactionType.AccountDelete:
return this.getAccountDeleteBuilder(tx);
case TransactionType.AccountUpdate:
return this.getAccountUpdateBuilder(tx);
case TransactionType.Send:
Expand All @@ -51,6 +54,11 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new WalletInitializationBuilder(this._coinConfig));
}

/** @inheritdoc */
public getAccountDeleteBuilder(tx?: Transaction): AccountDeleteBuilder {
return this.initializeBuilder(tx, new AccountDeleteBuilder(this._coinConfig));
}

/** @inheritdoc */
public getTransferBuilder(tx?: Transaction): TransferBuilder {
return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig));
Expand Down
Loading
Loading