All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.savl.ripple.client.transactions.TransactionManager Maven / Gradle / Ivy

There is a newer version: 1.0.2
Show newest version
package com.savl.ripple.client.transactions;

import com.savl.ripple.client.Client;
import com.savl.ripple.client.enums.Command;
import com.savl.ripple.client.pubsub.CallbackContext;
import com.savl.ripple.client.pubsub.Publisher;
import com.savl.ripple.client.requests.Request;
import com.savl.ripple.client.responses.Response;
import com.savl.ripple.client.subscriptions.ServerInfo;
import com.savl.ripple.client.subscriptions.TrackedAccountRoot;
import com.savl.ripple.core.coretypes.AccountID;
import com.savl.ripple.core.coretypes.Amount;
import com.savl.ripple.core.coretypes.hash.Hash256;
import com.savl.ripple.core.coretypes.uint.UInt32;
import com.savl.ripple.core.serialized.enums.EngineResult;
import com.savl.ripple.core.types.known.tx.Transaction;
import com.savl.ripple.core.types.known.tx.result.TransactionResult;
import com.savl.ripple.core.types.known.tx.txns.AccountSet;
import com.savl.ripple.crypto.ecdsa.IKeyPair;

import java.util.*;

public class TransactionManager extends Publisher {
    public static interface events extends Publisher.Callback {}
    // This event is emitted with the Sequence of the AccountRoot
    public static interface OnValidatedSequence extends events {}

    Client client;
    TrackedAccountRoot accountRoot;
    AccountID accountID;
    IKeyPair keyPair;
    AccountTxPager txnPager;

    private ArrayList pending = new ArrayList();
    private ArrayList failedTransactions = new ArrayList();

    public TransactionManager(Client client,
                              final TrackedAccountRoot accountRoot,
                              AccountID accountID,
                              IKeyPair keyPair) {
        this.client = client;
        this.accountRoot = accountRoot;
        this.accountID = accountID;
        this.keyPair = keyPair;

        // We'd be subscribed yeah ;)
        this.client.on(Client.OnLedgerClosed.class, new Client.OnLedgerClosed() {
            @Override
            public void called(ServerInfo serverInfo) {
                checkAccountTransactions(serverInfo.ledger_index);

                clearFailed(serverInfo.ledger_index);

                if (!canSubmit() || getPending().isEmpty()) {
                    return;
                }
                ArrayList sorted = pendingSequenceSorted();

                ManagedTxn first = sorted.get(0);
                Submission previous = first.lastSubmission();

                if (previous != null) {
                    long ledgersClosed = serverInfo.ledger_index - previous.ledgerSequence;
                    if (ledgersClosed > 5) {
                        resubmitWithSameSequence(first);
                    }
                }
            }
        });
    }

    private void clearFailed(long ledger_index) {
        // TODO: make sure each and every ledger has been checked
        int safety = 1;

        for (ManagedTxn failed : failedTransactions) {
            int expired = 0;
            for (Submission submission : failed.submissions) {
                if (ledger_index - safety > submission.lastLedgerSequence.longValue()) {
                    expired++;
                }
            }
            if (expired == failed.submissions.size()) {
                // The last response our submissions
                Response response = failed.lastSubmission().request.response;

                if (response != null) {
                    if (response.rpcerr != null) {
                        failed.emit(ManagedTxn.OnSubmitError.class, response);
                    } else {
                        failed.emit(ManagedTxn.OnSubmitFailure.class, response);
                    }
                }
            }

        }
    }


    Set seenValidatedSequences = new TreeSet();
    public long sequence = 0;

    private UInt32 locallyPreemptedSubmissionSequence() {
        if (!accountRoot.primed()) {
            throw new IllegalStateException("The AccountRoot hasn't been populated from the server");
        }
        long server = accountRoot.Sequence.longValue();
        if (server > sequence) {
            sequence = server;
        }
        return new UInt32(sequence++);
    }
    private boolean txnNotFinalizedAndSeenValidatedSequence(ManagedTxn txn) {
        return !txn.isFinalized() &&
               seenValidatedSequences.contains(txn.sequence().longValue());
    }

    public void queue(final ManagedTxn tx) {
        if (accountRoot.primed()) {
            queue(tx, locallyPreemptedSubmissionSequence());
        } else {
            accountRoot.once(TrackedAccountRoot.OnUpdate.class, new TrackedAccountRoot.OnUpdate() {
                @Override
                public void called(TrackedAccountRoot accountRoot) {
                    queue(tx, locallyPreemptedSubmissionSequence());
                }
            });
        }
    }

    // TODO: data structure that keeps txns in sequence sorted order
    public ArrayList getPending() {
        return pending;
    }

    public ArrayList pendingSequenceSorted() {
        ArrayList queued = new ArrayList(getPending());
        Collections.sort(queued, new Comparator() {
            @Override
            public int compare(ManagedTxn lhs, ManagedTxn rhs) {
                return lhs.sequence().compareTo(rhs.sequence());
            }
        });
        return queued;
    }

    public int txnsPending() {
        return getPending().size();
    }

    // TODO, maybe this is an instance configurable strategy parameter
    public static long LEDGERS_BETWEEN_ACCOUNT_TX = 15;
    public static long ACCOUNT_TX_TIMEOUT = 5;
    private long lastTxnRequesterUpdate = 0;
    private long lastLedgerCheckedAccountTxns = 0;
    AccountTxPager.OnPage onTxnsPage = new AccountTxPager.OnPage() {
        @Override
        public void onPage(AccountTxPager.Page page) {
            lastTxnRequesterUpdate = client.serverInfo.ledger_index;

            if (page.hasNext()) {
                page.requestNext();
            } else {
                lastLedgerCheckedAccountTxns = Math.max(lastLedgerCheckedAccountTxns, page.ledgerMax());
                txnPager = null;
            }

            for (TransactionResult tr : page.transactionResults()) {
                notifyTransactionResult(tr);
            }
        }
    };

    private void checkAccountTransactions(long currentLedgerIndex) {
        if (pending.size() == 0 && failedTransactions.size() == 0) {
            lastLedgerCheckedAccountTxns = 0;
            return;
        }

        long ledgersPassed = currentLedgerIndex - lastLedgerCheckedAccountTxns;

        if ((lastLedgerCheckedAccountTxns == 0 || ledgersPassed >= LEDGERS_BETWEEN_ACCOUNT_TX)) {
            if (lastLedgerCheckedAccountTxns == 0) {
                lastLedgerCheckedAccountTxns = currentLedgerIndex;
                for (ManagedTxn txn : pending) {
                    for (Submission submission : txn.submissions) {
                        lastLedgerCheckedAccountTxns = Math.min(lastLedgerCheckedAccountTxns, submission.ledgerSequence);
                    }
                }
                for (ManagedTxn txn : failedTransactions) {
                    for (Submission submission : txn.submissions) {
                        lastLedgerCheckedAccountTxns = Math.min(lastLedgerCheckedAccountTxns, submission.ledgerSequence);
                    }
                }
                return; // and wait for next ledger close
            }
            if (txnPager != null) {
                if ((currentLedgerIndex - lastTxnRequesterUpdate) >= ACCOUNT_TX_TIMEOUT) {
                    txnPager.abort(); // no more OnPage
                    txnPager = null; // and wait for next ledger close
                }
                // else keep waiting ;)
            } else {
                lastTxnRequesterUpdate = currentLedgerIndex;
                txnPager = new AccountTxPager(client, accountID, onTxnsPage,
                                                        /* for good measure */
                                                        lastLedgerCheckedAccountTxns - 5);

                // Very important VVVVV
                txnPager.forward(true);
                txnPager.request();
            }
        }
    }

    private void queue(final ManagedTxn txn, final UInt32 sequence) {
        getPending().add(txn);
        makeSubmitRequest(txn, sequence);
    }

    public boolean canSubmit() {
        return client.connected  &&
               client.serverInfo.primed() &&
                // ledger close could have given us
               client.serverInfo.fee_base != 0 &&
               client.serverInfo.load_factor < (768 * 1000 ) &&
               accountRoot.primed();
    }

    private void makeSubmitRequest(final ManagedTxn txn, final UInt32 sequence) {
        if (canSubmit()) {
            doSubmitRequest(txn, sequence);
        }
        else {
            // If we have submitted again, before this gets to execute
            // we should just bail out early, and not submit again.
            final int n = txn.submissions.size();
            client.on(Client.OnStateChange.class, new CallbackContext() {
                @Override
                public boolean shouldExecute() {
                    return canSubmit() && !shouldRemove();
                }

                @Override
                public boolean shouldRemove() {
                    // The next state change should cause this to remove
                    return txn.isFinalized() || n != txn.submissions.size();
                }
            }, new Client.OnStateChange() {
                @Override
                public void called(Client client) {
                    doSubmitRequest(txn, sequence);
                }
            });
        }
    }

    private Request doSubmitRequest(final ManagedTxn txn, UInt32 sequence) {
        // Compute the fee for the current load_factor
        Amount fee = client.serverInfo.transactionFee(txn.txn);
        // Inside prepare we check if Fee and Sequence are the same, and if so
        // we don't recreate tx_blob, or resign ;)


        long currentLedgerIndex = client.serverInfo.ledger_index;
        UInt32 lastLedgerSequence = new UInt32(currentLedgerIndex + 8);
        Submission submission = txn.lastSubmission();
        if (submission != null) {
            if (currentLedgerIndex - submission.lastLedgerSequence.longValue() < 8) {
                lastLedgerSequence = submission.lastLedgerSequence;
            }
        }

        txn.prepare(keyPair, fee, sequence, lastLedgerSequence);

        final Request req = client.newRequest(Command.submit);
        // tx_blob is a hex string, right o' the bat
        req.json("tx_blob", txn.tx_blob);

        req.once(Request.OnSuccess.class, new Request.OnSuccess() {
            @Override
            public void called(Response response) {
                handleSubmitSuccess(txn, response);
            }
        });

        req.once(Request.OnError.class, new Request.OnError() {
            @Override
            public void called(Response response) {
                handleSubmitError(txn, response);
            }
        });

        // Keep track of the submission, including the hash submitted
        // to the network, and the ledger_index at that point in time.
        txn.trackSubmitRequest(req, client.serverInfo.ledger_index);
        req.request();
        return req;
    }

    public void handleSubmitError(final ManagedTxn txn, Response res) {
        if (txn.finalizedOrResponseIsToPriorSubmission(res)) {
            return;
        }
        switch (res.rpcerr) {
            case noNetwork:
                client.schedule(500, new Runnable() {
                    @Override
                    public void run() {
                        resubmitWithSameSequence(txn);
                    }
                });
                break;
            default:
                // TODO, what other cases should this retry?
                // TODO, what if this actually eventually clears?
                // TOOD, need to use LastLedgerSequence
                awaitLastLedgerSequenceExpiry(txn);
                break;
        }
    }

    /**
     * We handle various transaction engine results specifically
     * and then by class of result.
     */
    public void handleSubmitSuccess(final ManagedTxn txn, final Response res) {
        if (txn.finalizedOrResponseIsToPriorSubmission(res)) {
            return;
        }
        EngineResult ter = res.engineResult();
        final UInt32 submitSequence = res.getSubmitSequence();
        switch (ter) {
            case tesSUCCESS:
                txn.emit(ManagedTxn.OnSubmitSuccess.class, res);
                return;
            case tefPAST_SEQ:
                resubmitWithNewSequence(txn);
                break;
            case tefMAX_LEDGER:
                resubmit(txn, submitSequence);
                break;
            case terPRE_SEQ:
                on(OnValidatedSequence.class, new OnValidatedSequence() {
                    @Override
                    public void called(UInt32 sequence) {
                        if (txn.finalizedOrResponseIsToPriorSubmission(res)) {
                            removeListener(OnValidatedSequence.class, this);
                        } else {
                            if (sequence.equals(submitSequence)) {
                                // resubmit:
                                resubmit(txn, submitSequence);
                                removeListener(OnValidatedSequence.class, this);
                            }
                        }
                    }
                });
                break;
            case telINSUF_FEE_P:
                resubmit(txn, submitSequence);
                break;
            case tefALREADY:
                // We only get this if we are submitting with exact same transactionID
                // Do nothing, the transaction has already been submitted
                break;
            default:
                switch (ter.resultClass()) {
                    case tecCLAIM:
                        // Sequence was consumed, may even succeed
                        // so do nothing, just wait until it clears or expires.
                        awaitLastLedgerSequenceExpiry(txn);
                        break;
                    // These are, according to the wiki, all of a final disposition
                    case temMALFORMED:
                    case tefFAILURE:
                    case telLOCAL_ERROR:
                    case terRETRY:
                        awaitLastLedgerSequenceExpiry(txn);
                        if (getPending().isEmpty()) {
                            sequence--;
                        } else {
                            // Plug a Sequence gap and pre-emptively resubmit some txns
                            // rather than waiting for `OnValidatedSequence` which will take
                            // quite some ledgers.
                            queueSequencePlugTxn(submitSequence);
                            resubmitGreaterThan(submitSequence);
                        }
                        break;
                }
                break;
        }
    }

    private void awaitLastLedgerSequenceExpiry(ManagedTxn txn) {
        finalizeTxnAndRemoveFromQueue(txn);
        failedTransactions.add(txn);
    }

    private void resubmitGreaterThan(UInt32 submitSequence) {
        for (ManagedTxn txn : getPending()) {
            if (txn.sequence().compareTo(submitSequence) == 1) {
                resubmitWithSameSequence(txn);
            }
        }
    }

    private void queueSequencePlugTxn(UInt32 sequence) {
        ManagedTxn plug = manage(new AccountSet());
        plug.setSequencePlug(true);
        queue(plug, sequence);
    }

    public void finalizeTxnAndRemoveFromQueue(ManagedTxn transaction) {
        transaction.setFinalized();
        pending.remove(transaction);
    }

    private void resubmitFirstTransactionWithTakenSequence(UInt32 sequence) {
        for (ManagedTxn txn : getPending()) {
            if (txn.sequence().compareTo(sequence) == 0) {
                resubmitWithNewSequence(txn);
                break;
            }
        }
    }
    // We only EVER resubmit a txn with a new Sequence if we have actually
    // seen that the Sequence has been consumed by a transaction we didn't
    // submit ourselves.
    // This is the method that handles that,
    private void resubmitWithNewSequence(final ManagedTxn txn) {
        // A sequence plug's sole purpose is to plug a Sequence
        // so that transactions may clear.
        if (txn.isSequencePlug()) {
            // The sequence has already been plugged (somehow)
            // So:
            return; // without further ado.
        }

        // ONLY ONLY ONLY if we've actually seen the Sequence
        if (txnNotFinalizedAndSeenValidatedSequence(txn)) {
            resubmit(txn, locallyPreemptedSubmissionSequence());
        } else {
            // requesting account_tx now and then (as we do) should ensure that
            // this doesn't stall forever. We'll either finalize the transaction
            // or Sequence will be seen to have been consumed by another txn.
            on(OnValidatedSequence.class,
                new CallbackContext() {
                    @Override
                    public boolean shouldExecute() {
                        return !txn.isFinalized();
                    }

                    @Override
                    public boolean shouldRemove() {
                        return txn.isFinalized();
                    }
                },
                new OnValidatedSequence() {
                    @Override
                    public void called(UInt32 uInt32) {
                        // Again, just to be safe.
                        if (txnNotFinalizedAndSeenValidatedSequence(txn)) {
                            resubmit(txn, locallyPreemptedSubmissionSequence());
                        }
                    }
            });
        }
    }

    private void resubmit(ManagedTxn txn, UInt32 sequence) {
//        if (txn.abortedAwaitingFinal()) {
//            return;
//        }
        makeSubmitRequest(txn, sequence);
    }

    private void resubmitWithSameSequence(ManagedTxn txn) {
        UInt32 previouslySubmitted = txn.sequence();
        resubmit(txn, previouslySubmitted);
    }

    public ManagedTxn manage(Transaction tt) {
        ManagedTxn txn = new ManagedTxn(tt);
        tt.account(accountID);
        return txn;
    }

    public void notifyTransactionResult(TransactionResult tr) {
        if (!tr.validated || !(tr.initiatingAccount().equals(accountID))) {
            return;
        }
        UInt32 txnSequence = tr.txn.get(UInt32.Sequence);
        seenValidatedSequences.add(txnSequence.longValue());

        ManagedTxn txn = submittedTransactionForHash(tr.hash);
        if (txn != null) {
            finalizeTxnAndRemoveFromQueue(txn);
            failedTransactions.remove(txn);
            txn.emit(ManagedTxn.OnTransactionValidated.class, tr);
        } else {
            // TODO Check for transaction malleability, by computing a signing hash
            // preempt the terPRE_SEQ
            resubmitFirstTransactionWithTakenSequence(txnSequence);
            // Some transactions are waiting on this event before resubmission
            emit(OnValidatedSequence.class, txnSequence.add(new UInt32(1)));
        }
    }

    private ManagedTxn submittedTransactionForHash(Hash256 hash) {
        for (ManagedTxn pending : getPending()) {
            if (pending.wasSubmittedWith(hash)) {
                return pending;
            }
        }
        for (ManagedTxn markedAsFailed : failedTransactions) {
            if (markedAsFailed.wasSubmittedWith(hash)) {
                return markedAsFailed;
            }
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy