Skip to content
Open
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
164 changes: 164 additions & 0 deletions core/src/main/java/org/stellar/anchor/ledger/LedgerClientHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.function.Function;
import org.stellar.anchor.api.exception.LedgerException;
import org.stellar.anchor.util.AssetHelper;
import org.stellar.sdk.MuxedAccount;
import org.stellar.sdk.StrKey;
import org.stellar.sdk.TOID;
import org.stellar.sdk.exception.BadRequestException;
Expand Down Expand Up @@ -346,6 +350,18 @@ public static List<LedgerOperation> getLedgerOperations(
ParseResult parseResult,
OperationResult[] operationResults)
throws LedgerException {
return getLedgerOperations(
applicationOrder, sequenceNumber, parseResult, operationResults, null, null);
}

public static List<LedgerOperation> getLedgerOperations(
Integer applicationOrder,
Long sequenceNumber,
ParseResult parseResult,
OperationResult[] operationResults,
List<List<ContractEvent>> perOperationContractEvents,
Function<String, Asset> sacResolver)
throws LedgerException {
if (operationResults != null && operationResults.length != parseResult.operations().length) {
throw new LedgerException(
"Operation/result count mismatch ("
Expand All @@ -365,13 +381,161 @@ public static List<LedgerOperation> getLedgerOperations(
opIndex + 1, // operation index is 1-based
parseResult.operations()[opIndex],
opResult);
List<ContractEvent> contractEvents =
perOperationContractEvents != null && opIndex < perOperationContractEvents.size()
? perOperationContractEvents.get(opIndex)
: null;
if (contractEvents != null
&& (ledgerOp == null || ledgerOp.getType() == INVOKE_HOST_FUNCTION)) {
List<LedgerOperation> verifiedTransfers =
synthesizeVerifiedTransfers(
parseResult.sourceAccount(),
sequenceNumber,
applicationOrder,
opIndex + 1,
contractEvents,
sacResolver);
operations.addAll(verifiedTransfers);
continue;
}
if (ledgerOp != null) {
operations.add(ledgerOp);
}
}
return operations;
}

private static List<LedgerOperation> synthesizeVerifiedTransfers(
String sourceAccount,
Long sequenceNumber,
Integer applicationOrder,
int opIndex,
List<ContractEvent> contractEvents,
Function<String, Asset> sacResolver) {
List<LedgerOperation> verified = new ArrayList<>();
if (contractEvents == null || sacResolver == null) {
return verified;
}
String operationId =
String.valueOf(new TOID(sequenceNumber.intValue(), applicationOrder, opIndex).toInt64());
for (ContractEvent event : contractEvents) {
TransferEventData data = parseTransferEvent(event);
Comment thread
amandagonsalves marked this conversation as resolved.
if (data == null) {
continue;
}
String contractId;
try {
contractId = StrKey.encodeContract(event.getContractID().toXdrByteArray());
} catch (IOException ioex) {
continue;
}
Asset canonicalAsset = sacResolver.apply(contractId);
if (canonicalAsset == null) {
continue;
}
String canonicalSep11Asset = AssetHelper.getSep11AssetName(canonicalAsset);
if (!canonicalSep11Asset.equals(data.sep11Asset())) {
continue;
}
verified.add(
LedgerOperation.builder()
.type(INVOKE_HOST_FUNCTION)
.invokeHostFunctionOperation(
LedgerInvokeHostFunctionOperation.builder()
.id(operationId)
.contractId(contractId)
.hostFunction("transfer")
.from(data.fromAddr())
.to(data.toAddr())
.amount(data.amount())
.asset(canonicalAsset)
.sourceAccount(sourceAccount)
.build())
.build());
}
return verified;
}

public record TransferEventData(
String fromAddr, String toAddr, BigInteger amount, String sep11Asset, String eventMemo) {}

public static TransferEventData parseTransferEvent(ContractEvent event) {
if (event.getType() != ContractEventType.CONTRACT || event.getBody().getV0() == null) {
return null;
}
SCVal[] topics = event.getBody().getV0().getTopics();
if (topics == null || topics.length != 4) {
return null;
}

SCVal function = topics[0];
SCVal from = topics[1];
SCVal to = topics[2];
SCVal asset = topics[3];

if (function.getDiscriminant() != SCValType.SCV_SYMBOL
|| !function.getSym().getSCSymbol().toString().equals("transfer")) {
return null;
}
if (from.getDiscriminant() != SCValType.SCV_ADDRESS
|| to.getDiscriminant() != SCValType.SCV_ADDRESS
|| asset.getDiscriminant() != SCValType.SCV_STRING) {
return null;
}

String fromAddr;
String toAddr;
try {
fromAddr = Scv.fromAddress(from).toString();
toAddr = Scv.fromAddress(to).toString();
} catch (RuntimeException ex) {
return null;
}

BigInteger amount;
String eventMemo = null;
SCVal scValue = event.getBody().getV0().getData();
if (scValue.getDiscriminant() == SCValType.SCV_I128) {
amount = Scv.fromInt128(scValue);
} else if (scValue.getDiscriminant() == SCValType.SCV_MAP) {
var entries = scValue.getMap() == null ? null : scValue.getMap().getSCMap();
if (entries == null || entries.length < 2) {
return null;
}
SCVal amountVal = entries[0].getVal();
SCVal memoVal = entries[1].getVal();
if (amountVal.getDiscriminant() != SCValType.SCV_I128) {
return null;
}
amount = Scv.fromInt128(amountVal);
eventMemo =
switch (memoVal.getDiscriminant()) {
case SCV_STRING -> memoVal.getStr().getSCString().toString();
case SCV_U64 -> memoVal.getU64().toString();
case SCV_BYTES ->
new String(Base64.getEncoder().encode(memoVal.getBytes().getSCBytes()));
default -> null;
};
Comment on lines +505 to +518
if (memoVal.getDiscriminant() == SCValType.SCV_U64) {
try {
toAddr =
new MuxedAccount(Scv.fromAddress(to).toString(), Scv.fromUint64(memoVal))
.getAddress();
} catch (IllegalArgumentException iae) {
warnF(
"Cannot build MuxedAccount for address '{}', using unmuxed value. ex={}",
toAddr,
iae.getMessage());
}
}
} else {
return null;
}

return new TransferEventData(
fromAddr, toAddr, amount, asset.getStr().getSCString().toString(), eventMemo);
}

public static LedgerTransaction waitForTransactionAvailable(
LedgerClient ledgerClient, String txhHash) throws LedgerException {
return waitForTransactionAvailable(ledgerClient, txhHash, 10, 10);
Expand Down
34 changes: 32 additions & 2 deletions core/src/main/java/org/stellar/anchor/ledger/StellarRpc.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.stellar.anchor.ledger;

import static org.stellar.anchor.util.Log.debugF;
import static org.stellar.sdk.xdr.LedgerEntry.*;
import static org.stellar.sdk.xdr.SignerKeyType.SIGNER_KEY_TYPE_ED25519;
import static org.stellar.sdk.xdr.SignerKeyType.SIGNER_KEY_TYPE_ED25519_SIGNED_PAYLOAD;
Expand Down Expand Up @@ -33,6 +34,12 @@
public class StellarRpc implements LedgerClient {
String rpcServerUrl;
@Getter SorobanServer sorobanServer;
java.util.function.Function<String, org.stellar.sdk.xdr.Asset> sacResolver = contractId -> null;

public void setSacResolver(
java.util.function.Function<String, org.stellar.sdk.xdr.Asset> sacResolver) {
this.sacResolver = sacResolver;
}

public StellarRpc(String rpcServerUrl) {
this.rpcServerUrl = rpcServerUrl;
Expand Down Expand Up @@ -170,7 +177,7 @@ public LedgerTransaction getTransaction(String txnHash) throws LedgerException {
return switch (txn.getStatus()) {
case NOT_FOUND -> null;
case FAILED -> throw new LedgerException("Error getting transaction: " + txnHash);
case SUCCESS -> fromGetTransactionResponse(txn);
case SUCCESS -> fromGetTransactionResponse(txn, sacResolver);
};
}

Expand Down Expand Up @@ -241,6 +248,13 @@ private AccountEntry getAccountRpc(String accountId) throws IOException, LedgerE
*/
public static LedgerTransaction fromGetTransactionResponse(GetTransactionResponse txnResponse)
throws LedgerException {
return fromGetTransactionResponse(txnResponse, contractId -> null);
}

public static LedgerTransaction fromGetTransactionResponse(
GetTransactionResponse txnResponse,
java.util.function.Function<String, org.stellar.sdk.xdr.Asset> sacResolver)
throws LedgerException {
TransactionEnvelope txnEnv;
try {
txnEnv = TransactionEnvelope.fromXdrBase64(txnResponse.getEnvelopeXdr());
Expand All @@ -262,9 +276,25 @@ public static LedgerTransaction fromGetTransactionResponse(GetTransactionRespons
}
OperationResult[] opResults =
LedgerClientHelper.parseOperationResults(txResult, txnResponse.getTxHash());
List<List<ContractEvent>> perOperationContractEvents = null;
if (txnResponse.getEvents() != null) {
try {
perOperationContractEvents = txnResponse.getEvents().parseContractEventsXdr();
} catch (RuntimeException rex) {
debugF(
"Unable to parse contract events for hash={}: {}",
txnResponse.getTxHash(),
rex.getMessage());
}
}
List<LedgerTransaction.LedgerOperation> operations =
LedgerClientHelper.getLedgerOperations(
applicationOrder, sequenceNumber, parseResult, opResults);
applicationOrder,
sequenceNumber,
parseResult,
opResults,
perOperationContractEvents,
sacResolver);

return LedgerTransaction.builder()
.hash(txnResponse.getTxHash())
Expand Down
Loading
Loading