Skip to content

[ANCHOR-916] Put Horizon behind interface #1624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
74 changes: 0 additions & 74 deletions core/src/main/java/org/stellar/anchor/horizon/Horizon.java

This file was deleted.

145 changes: 145 additions & 0 deletions core/src/main/java/org/stellar/anchor/ledger/Horizon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.stellar.anchor.ledger;

import static org.stellar.anchor.api.asset.AssetInfo.NATIVE_ASSET_CODE;

import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;
import org.stellar.anchor.config.AppConfig;
import org.stellar.anchor.ledger.LedgerTransaction.LedgerTransactionResponse;
import org.stellar.anchor.util.AssetHelper;
import org.stellar.sdk.*;
import org.stellar.sdk.exception.NetworkException;
import org.stellar.sdk.requests.PaymentsRequestBuilder;
import org.stellar.sdk.responses.AccountResponse;
import org.stellar.sdk.responses.TransactionResponse;
import org.stellar.sdk.responses.operations.OperationResponse;
import org.stellar.sdk.xdr.AssetType;

/** The horizon-server. */
public class Horizon implements LedgerApi {

@Getter private final String horizonUrl;
@Getter private final String stellarNetworkPassphrase;
private final Server horizonServer;

public Horizon(AppConfig appConfig) {
this.horizonUrl = appConfig.getHorizonUrl();
this.stellarNetworkPassphrase = appConfig.getStellarNetworkPassphrase();
this.horizonServer = new Server(appConfig.getHorizonUrl());
}

public Server getServer() {
return this.horizonServer;
}

public boolean hasTrustline(String account, String asset) throws NetworkException {
String assetCode = AssetHelper.getAssetCode(asset);
if (NATIVE_ASSET_CODE.equals(assetCode)) {
return true;
}
String assetIssuer = AssetHelper.getAssetIssuer(asset);

AccountResponse accountResponse = getServer().accounts().account(account);
return accountResponse.getBalances().stream()
.anyMatch(
balance -> {
TrustLineAsset trustLineAsset = balance.getTrustLineAsset();
if (trustLineAsset.getAssetType() == AssetType.ASSET_TYPE_CREDIT_ALPHANUM4
|| trustLineAsset.getAssetType() == AssetType.ASSET_TYPE_CREDIT_ALPHANUM12) {
AssetTypeCreditAlphaNum creditAsset =
(AssetTypeCreditAlphaNum) trustLineAsset.getAsset();
assert creditAsset != null;
return creditAsset.getCode().equals(assetCode)
&& creditAsset.getIssuer().equals(assetIssuer);
}
return false;
});
}

@Override
public Account getAccount(String account) throws NetworkException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we only return the account signers in this method? The balances field is already covered by the hasTrustlines method. Implementing this RPC would mean making two GetLedgerEntries calls when most of the time, we would only care about signers or trustlines/balances but not both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is based on RPC ledger endpoint. It returns both signers and trustlines.

The GetLedgerEntries takes a List<LedgerKey>. This is only one call though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the hasTrustline should be a helper function instead of a interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is based on RPC ledger endpoint. It returns both signers and trustlines.

The GetLedgerEntries takes a List. This is only one call though.

Ah, that's right. Do we need balance, though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I will remove.

AccountResponse response = getServer().accounts().account(account);
AccountResponse.Thresholds thresholds = response.getThresholds();

return Account.builder()
.accountId(response.getAccountId())
.sequenceNumber(response.getSequenceNumber())
.thresholds(
LedgerApi.Thresholds.builder()
.lowThreshold(thresholds.getLowThreshold())
.medThreshold(thresholds.getMedThreshold())
.highThreshold(thresholds.getHighThreshold())
.build())
.balances(
response.getBalances().stream()
.map(
b ->
Balance.builder()
.assetType(b.getAssetType())
.assetCode(b.getAssetCode())
.assetIssuer(b.getAssetIssuer())
.liquidityPoolId(b.getLiquidityPoolId())
.limit(b.getLimit())
.build())
.collect(Collectors.toList()))
.signers(
response.getSigners().stream()
.map(
s ->
Signer.builder()
.key(s.getKey())
.type(s.getType())
.weight(s.getWeight())
.sponsor(s.getSponsor())
.build())
.collect(Collectors.toList()))
.build();
}

@Override
public LedgerTransaction getTransaction(String transactionId) throws NetworkException {
TransactionResponse response = getServer().transactions().transaction(transactionId);
return LedgerTransaction.builder()
.hash(response.getHash())
.sourceAccount(response.getSourceAccount())
.envelopeXdr(response.getEnvelopeXdr())
.metaXdr(response.getResultMetaXdr())
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these fields?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we do. ObservedPayment.java

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use resultMetaXdr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we currently don't. Considering reducing the maintainance load, I will remove

.sourceAccount(response.getSourceAccount())
.memo(response.getMemo())
.sequenceNumber(response.getSourceAccountSequence())
.createdAt(response.getCreatedAt())
.build();
}

@Override
public LedgerTransactionResponse submitTransaction(Transaction transaction)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to implement a submitTransaction method if we only submit transactions in tests? I don't think we use the Horizon class today. Can we remove this method until we need to submit transactions? Otherwise, we have to deal with the maintenance burden of a method we don't use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, we discussed about having submission function that may be required if we are expanding the observer to txn submitter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have this planned right now, so I think it makes sense to hold off on the implementation until then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation was there and the tests needs it. We need submit for both RPC and horizon for testing.

throws NetworkException {
TransactionResponse txnR = getServer().submitTransaction(transaction, false);

return LedgerTransactionResponse.builder()
.hash(txnR.getHash())
.metaXdr(txnR.getEnvelopeXdr())
.envelopXdr(txnR.getEnvelopeXdr())
.sourceAccount(txnR.getSourceAccount())
.feeCharged(txnR.getFeeCharged().toString())
.createdAt(txnR.getCreatedAt())
.build();
}

/**
* Get payment operations for a transaction.
*
* @param stellarTxnId the transaction id
* @return the operations
* @throws NetworkException request failed, see {@link PaymentsRequestBuilder#execute()}
*/
public List<OperationResponse> getStellarTxnOperations(String stellarTxnId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the OperationResponse specific to Horizon's API?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should this method be defined in the interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the OperationResponse is specific to Horizon's API only. The observer's logics for RPC and Horizon are very different. It won't fit in the interface.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the plan to use the interface in the observer then?

Copy link
Contributor Author

@lijamie98 lijamie98 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if possible, I would use interface. If too different, then have to use classes for observers.

return getServer()
.payments()
.includeTransactions(true)
.forTransaction(stellarTxnId)
.execute()
.getRecords();
}
}
110 changes: 110 additions & 0 deletions core/src/main/java/org/stellar/anchor/ledger/LedgerApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.stellar.anchor.ledger;

import java.io.IOException;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Value;
import org.stellar.sdk.KeyPair;
import org.stellar.sdk.Transaction;
import org.stellar.sdk.TransactionBuilderAccount;
import org.stellar.sdk.exception.NetworkException;

public interface LedgerApi {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we call this something else? Maybe StellarNetwork?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like LedgerApi and StellarNetwork which conflicts with many other things though. But if to let me choose, by looking at the functions, I would prefer LedgerApi.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about StellarClient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a better one?....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StellarNetworkAdapter or StellarNetworkClient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StellarClient is too close to others. StellarNetworkAdapter or StellarNetworkClient are quite close.

The functions are

  • getTransaction
  • getAccount (signers, trustlines, balance(optional))
  • submitTransaction

I think LedgerApi is not perfect but close.

How about LedgerClient?

/**
* Check if the account has a trustline for the given asset.
*
* @param account The account to check.
* @param asset The asset to check.
* @return True if the account has a trustline for the asset.
* @throws NetworkException If there was an error communicating with the network.
*/
boolean hasTrustline(String account, String asset) throws NetworkException, IOException;

/**
* Get the account details for the given account.
*
* @param account The account to get.
* @return The account details.
* @throws NetworkException If there was an error communicating with the network.
*/
Account getAccount(String account) throws NetworkException;

/**
* Get the operations for the given Stellar transaction.
*
* @param stellarTxnId The Stellar transaction ID.
* @return The operations for the transaction.
*/
LedgerTransaction getTransaction(String stellarTxnId);

/**
* Submit a transaction to the network.
*
* @param transaction The transaction to submit.
* @return The transaction response.
* @throws NetworkException If there was an error communicating with the network.
*/
LedgerTransaction.LedgerTransactionResponse submitTransaction(Transaction transaction)
throws NetworkException;

@Builder
@Getter
class Account implements TransactionBuilderAccount {
private String accountId;
private Long sequenceNumber;

private Thresholds thresholds;
private List<Balance> balances;
private List<Signer> signers;

@Override
public KeyPair getKeyPair() {
return KeyPair.fromAccountId(accountId);
}

@Override
public void setSequenceNumber(long seqNum) {
sequenceNumber = seqNum;
}

@Override
public Long getIncrementedSequenceNumber() {
return sequenceNumber + 1;
}

/** Increments sequence number in this object by one. */
public void incrementSequenceNumber() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required by the TransactionBuilderAccount interface.

sequenceNumber++;
}
}

@Builder
@Getter
class Thresholds {
Integer lowThreshold;
Integer medThreshold;
Integer highThreshold;
}

@Builder
@Getter
class Balance {
String assetType;
String assetCode;
String assetIssuer;
String liquidityPoolId;
String limit;
String balance;
}

@Value
@Builder
@Getter
class Signer {
String key;
String type;
Integer weight;
String sponsor;
}
}
Loading
Loading