Skip to content

Commit

Permalink
Issue #47 fix. Implemented from scratch the TX in/out tokens calculation
Browse files Browse the repository at this point in the history
Signed-off-by: Alessio <[email protected]>
  • Loading branch information
slux83 committed Feb 7, 2024
1 parent 491757c commit 71816b9
Show file tree
Hide file tree
Showing 8 changed files with 954 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class TransactionCheckerTaskV2 extends AbstractCheckerTask implements Run
private static final String DELEGATION_CERTIFICATE = "delegation";
private static final String BLOCK_HEIGHT_FIELD = "block_height";
private static final int MAX_TX_IN_TELEGRAM_NOTIFICATION = 3;

@Value("${thoth.test.allow-jumbo-message}")
private Boolean allowJumboMessage;

Expand Down Expand Up @@ -342,11 +343,53 @@ else if (!allInputAddresses.contains(user.getAddress()))
.filter(tx -> user.getAddress().equals(tx.getStakeAddr()) || user.getAddress().equals(tx.getPaymentAddr().getBech32()))
.flatMap(io -> io.getAssetList().stream()).collect(Collectors.toSet());

outputAssets.removeIf(a -> inputAssets.stream().map(Asset::getFingerprint).anyMatch(x -> x.equals(a.getFingerprint())));
Map<String, Double> inputAssetValues = new HashMap<>();
for (Asset ia : inputAssets) {
Double val = inputAssetValues.getOrDefault(ia.getFingerprint(), 0d);
val += Long.parseLong(ia.getQuantity()) / Math.pow(10, ia.getDecimals());
inputAssetValues.put(ia.getFingerprint(), val);
}

Map<String, Double> outputAssetValues = new HashMap<>();
for (Asset oa : outputAssets) {
Double val = outputAssetValues.getOrDefault(oa.getFingerprint(), 0d);
val += Long.parseLong(oa.getQuantity()) / Math.pow(10, oa.getDecimals());
outputAssetValues.put(oa.getFingerprint(), val);
}

Map<String, Double> assetValues = new HashMap<>(inputAssetValues);
// Input tokens shall be negative because they are leaving the wallet
assetValues.replaceAll((k, v) -> v *= -1);

List<Asset> allAssets = accountOutputs.stream().flatMap(tx -> tx.getAssetList().stream()).collect(Collectors.toList());
for (Map.Entry<String, Double> a : outputAssetValues.entrySet()) {
if (assetValues.containsKey(a.getKey()))
assetValues.put(a.getKey(), assetValues.get(a.getKey()) + a.getValue());
else
assetValues.put(a.getKey(), a.getValue());
}

LOG.debug("All assets:\n{}\noutput assets:\n{}", allAssets, outputAssets);
Map<Asset, Number> allAssets = new HashMap<>();
for (Map.Entry<String, Double> av : assetValues.entrySet()) {
// find the asset in input or output
Optional<Asset> asset = inputAssets.stream().filter(a -> a.getFingerprint().equals(av.getKey())).findFirst();
if (asset.isEmpty())
asset = outputAssets.stream().filter(a -> a.getFingerprint().equals(av.getKey())).findFirst();

if (asset.isEmpty()) {
LOG.error("Cannot find the asset with fingerprint {} among input/output lists, in the Tx {}",
av.getKey(), txInfo.getTxHash());
continue;
}

if (av.getValue() != 0d) {
if (asset.get().getDecimals() > 0)
allAssets.put(asset.get(), av.getValue());
else
allAssets.put(asset.get(), av.getValue().longValue());
}
}

LOG.debug("All assets in TX: {}", assetValues);

double receivedOrSentFunds = accountOutputs.stream().mapToLong(tx -> Long.parseLong(tx.getValue())).sum() / LOVELACE;

Expand Down Expand Up @@ -380,7 +423,7 @@ else if (!allInputAddresses.contains(user.getAddress()))
List<PoolInfo> poolInfoList = null;

try {
Result<List<PoolInfo>> poolInfoRes = this.koiosFacade.getKoiosService().getPoolService().getPoolInformation(Arrays.asList(delegateToPoolId), options);
Result<List<PoolInfo>> poolInfoRes = this.koiosFacade.getKoiosService().getPoolService().getPoolInformation(Collections.singletonList(delegateToPoolId), options);
if (poolInfoRes.isSuccessful()) poolInfoList = poolInfoRes.getValue();
else LOG.warn("Cannot retrieve pool information due to {}", poolInfoRes.getResponse());
} catch (ApiException e) {
Expand Down Expand Up @@ -411,11 +454,11 @@ else if (!allInputAddresses.contains(user.getAddress()))
if (LOG.isDebugEnabled()) {
LOG.debug("TX {} Amount {} Fees {} USD Price {} Pool Delegation {} ({}) Message {} Assets {}",
txInfo.getTxHash(), receivedOrSentFunds, fee, latestCardanoPriceUsd, delegateToPoolName, delegateToPoolId,
metadataMessage, allAssets.stream().map(Asset::getFingerprint).collect(Collectors.joining("\n")));
metadataMessage, allAssets.keySet().stream().map(Asset::getFingerprint).collect(Collectors.joining("\n")));
}

StringBuilder sb = new StringBuilder();
renderSingleTransactionMessage(sb, txInfo, allAssets, outputAssets, txType, latestCardanoPriceUsd, fee,
renderSingleTransactionMessage(sb, txInfo, allAssets, txType, latestCardanoPriceUsd, fee,
receivedOrSentFunds, delegateToPoolName, delegateToPoolId, metadataMessage, totalWithdrawals);
return sb;
}
Expand Down Expand Up @@ -463,13 +506,24 @@ private StringBuilder renderTransactionMessageHeader(User u, Map<String, String>
}

private StringBuilder renderSingleTransactionMessage(StringBuilder messageBuilder, TxInfo txInfo,
List<Asset> allAssets, Set<Asset> outputAssets, TxType txType,
Map<Asset, Number> allAssets, TxType txType,
Double latestCardanoPriceUsd, Double fee, double receivedOrSentFunds,
String delegateToPoolName, String delegateToPoolId, String metadataMessage,
double totalWithdrawals) {
String fundsTokenText = String.format("Funds %s", allAssets.isEmpty() ? "" : "and Tokens");
if (!outputAssets.isEmpty())

String fundsTokenText = "";
// Check how many received and sent tokens we have (negative is sent, positive is received)
boolean sentTokens = allAssets.values().stream().anyMatch(v -> v.doubleValue() < 0);
boolean receivedTokens = allAssets.values().stream().anyMatch(v -> v.doubleValue() > 0);

if (sentTokens && receivedTokens)
fundsTokenText = "Funds, Sent and Received Tokens";
else if (sentTokens)
fundsTokenText = "Funds and Sent Tokens";
else if (receivedTokens)
fundsTokenText = "Funds and Received Tokens";
else
fundsTokenText = "Funds";

switch (txType) {
case TX_RECEIVED:
Expand All @@ -480,7 +534,7 @@ private StringBuilder renderSingleTransactionMessage(StringBuilder messageBuilde
break;
case TX_INTERNAL:
messageBuilder.append(EmojiParser.parseToUnicode(":repeat: "));
fundsTokenText = String.format("Transfer %s", allAssets.isEmpty() ? "" : "and Tokens");
fundsTokenText += " (internal)";
break;
}

Expand Down Expand Up @@ -573,49 +627,26 @@ private StringBuilder renderSingleTransactionMessage(StringBuilder messageBuilde
}

// Any assets?
Collection<Asset> uniqueAssets = allAssets.stream().collect(Collectors.toMap(Asset::getFingerprint, a -> a, (a, b) -> a)).values();
Map<String, Long> quantities = computeAssetsQuantity(allAssets);

if (!uniqueAssets.isEmpty()) {
for (Asset asset : uniqueAssets) {
Object assetQuantity = null;
String assetName = hexToAscii(asset.getAssetName(), asset.getPolicyId());
try {
assetQuantity = this.assetFacade.getAssetQuantity(asset.getPolicyId(), asset.getAssetName(),
quantities.get(asset.getFingerprint()));
assetName = this.assetFacade.getAssetDisplayName(asset.getPolicyId(), asset.getAssetName());
} catch (ApiException e) {
LOG.warn("Could not get the asset quantity for asset {}/{}: {}",
asset.getPolicyId(), asset.getAssetName(), e.toString());
}

messageBuilder
.append(EmojiParser.parseToUnicode("\n:small_orange_diamond:"))
.append(assetName).append(" ")
.append(this.assetFacade.formatAssetQuantity(assetQuantity));
for (Map.Entry<Asset, Number> asset : allAssets.entrySet()) {
String assetName = hexToAscii(asset.getKey().getAssetName(), asset.getKey().getPolicyId());
try {
assetName = this.assetFacade.getAssetDisplayName(asset.getKey().getPolicyId(), asset.getKey().getAssetName());
} catch (ApiException e) {
LOG.warn("Could not get the asset quantity for asset {}/{}: {}",
asset.getKey().getPolicyId(), asset.getKey().getAssetName(), e.toString());
}

messageBuilder
.append(EmojiParser.parseToUnicode("\n:small_orange_diamond:"))
.append(assetName).append(" ")
.append(this.assetFacade.formatAssetQuantity(asset.getValue()));
}

messageBuilder.append("\n\n"); // Some padding between TXs

return messageBuilder;
}

private Map<String, Long> computeAssetsQuantity(List<Asset> assets) {
Map<String, Long> assetsQuantity = new HashMap<>();

for (Asset asset : assets) {
Long assetQuantity = Long.parseLong(asset.getQuantity());
if (!assetsQuantity.containsKey(asset.getFingerprint())) {
assetsQuantity.put(asset.getFingerprint(), assetQuantity);
} else {
assetsQuantity.put(asset.getFingerprint(), assetsQuantity.get(asset.getFingerprint()) + assetQuantity);
}
}

return assetsQuantity;
}

public Map<String, String> getContracts() {
return contracts;
}
Expand Down
42 changes: 27 additions & 15 deletions src/test/java/com/devpool/thothBot/IntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public void scheduledNotificationsTest() throws Exception {
Assertions.assertEquals(87, countTxForAddress(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32"));
Assertions.assertEquals(41, countTxForAddress(allMessages, "stake1u9ttjzthgk2y7x55c9f363a6vpcthv0ukl2d5mhtxvv4kusv5fmtz"));
Assertions.assertEquals(16, countTxForAddress(allMessages, "stake1uxpdrerp9wrxunfh6ukyv5267j70fzxgw0fr3z8zeac5vyqhf9jhy"));
Assertions.assertEquals(1, countTxForAddress(allMessages, "stake1u8656c05pay70xtpcwp3dqgu4jwullv6qu9e50ykn59lz7g7vzwt7"));
Assertions.assertEquals(2, countTxForAddress(allMessages, "stake1u8656c05pay70xtpcwp3dqgu4jwullv6qu9e50ykn59lz7g7vzwt7"));
Assertions.assertEquals(5, countTxForAddress(allMessages, "addr1qy2jt0qpqz2z2z9zx5w4xemekkce7yderz53kjue53lpqv90lkfa9sgrfjuz6uvt4uqtrqhl2kj0a9lnr9ndzutx32gqleeckv"));
Assertions.assertEquals(58, countTxForAddress(allMessages, "addr1wxwrp3hhg8xdddx7ecg6el2s2dj6h2c5g582yg2yxhupyns8feg4m"));
Assertions.assertEquals(4, allMessages.stream().filter(m -> m.contains("reward(s)")).count());
Expand Down Expand Up @@ -341,32 +341,34 @@ public void scheduledNotificationsTest() throws Exception {
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32", "3e2e4a2b7d78bc5994773805f1376d790c8169b63297d50ef4842e22aafb1f29");
Assertions.assertTrue(message.contains("Fee 0.39"));
Assertions.assertTrue(message.contains("Sent -5.57"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 1939 1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 3022 1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 2884 1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 5802 1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 1939 -1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 3022 -1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 2884 -1"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 5802 -1"));

// TX sent funds, jpeg store contract
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32", "e9f577499a692fc07491cd7de013ea2c3b3a37b3df616aeb39f807ed5ced8d24");
Assertions.assertTrue(message.contains("Fee 0.46"));
Assertions.assertTrue(message.contains("Sent -13.61"));
Assertions.assertTrue(message.contains("JpegStore"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 1501 1"));

// TX received funds, jpeg store multiple (2) contracts
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32", "b6170c1c89f91bb5f76c0810889ea110f34b63e7fde25b37abe269256ac2f45a");
Assertions.assertTrue(message.contains("Fee 1.11"));
Assertions.assertTrue(message.contains("Received 18.00"));
message = message.substring(message.indexOf("b6170c1c89f91bb5f76c0810889ea110f34b63e7fde25b37abe269256ac2f45a"));
Assertions.assertEquals(2, message.split("JpegStore").length - 1);
Assertions.assertEquals(2, message.split("\\[JpegStore]").length - 1);

// TX with ada handle
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32", "a6abf48aad975fac80416ce79f9a7969fe05e13a37eb8be1e917d5d84d6044");
Assertions.assertTrue(message.contains("$alessio.dev"));

// TX sent funds and tokens (with token hash)
message = retrieveMessageByString(allMessages, "stake1u9ttjzthgk2y7x55c9f363a6vpcthv0ukl2d5mhtxvv4kusv5fmtz", "69c2f2f96305b5d1eb46eb5180f9dfb0409c54919d2463ae61becf34570e504a");
Assertions.assertTrue(message.contains("...9d660cf6a2 498,221,110"));
Assertions.assertTrue(message.contains("hvADA 2,528,098"));
// TX sent funds and received tokens
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32",
"2249e6906a7fba98247f22939ee102eb0ceeea207d3014a3b2cbd4944dd21513");
Assertions.assertTrue(message.contains("Sent 1.39"));
Assertions.assertTrue(message.contains("Cardano Summit 2023 NFT 6808 1"));

// Pool operator rewards
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32",
Expand All @@ -393,7 +395,7 @@ public void scheduledNotificationsTest() throws Exception {
// TX received funds, received tokens
message = retrieveMessageByString(allMessages, "stake1uxpdrerp9wrxunfh6ukyv5267j70fzxgw0fr3z8zeac5vyqhf9jhy",
"083e302d0c5d11c07fa642c18df8fa290632c24157c676a754a5a763605ebe26");
Assertions.assertTrue(message.contains("MACH 3,947.00"));
Assertions.assertTrue(message.contains("MACH 3,947"));
Assertions.assertTrue(message.contains("1.73"));
Assertions.assertTrue(message.contains("Received Funds and Received Tokens"));
Assertions.assertFalse(message.contains("[JpegStore]"));
Expand All @@ -416,18 +418,28 @@ public void scheduledNotificationsTest() throws Exception {
message = retrieveMessageByString(allMessages, "stake1u8656c05pay70xtpcwp3dqgu4jwullv6qu9e50ykn59lz7g7vzwt7",
"f5401d48ac42a1199c8fbb214e63e4f350ee5a4f099ff460ca7f8f7bdcfabd4c");

Assertions.assertTrue(message.contains("Sent Funds and Tokens"));
Assertions.assertTrue(message.contains("Djed USD 746.00"));
Assertions.assertTrue(message.contains("Sent Funds and Sent Tokens"));
Assertions.assertTrue(message.contains("Djed USD -746.00"));
Assertions.assertTrue(message.contains("-1.19"));
Assertions.assertFalse(message.contains("iETH"));

// Issue #39
message = retrieveMessageByString(allMessages, "stake1u8uekde7k8x8n9lh0zjnhymz66sqdpa0ms02z8cshajptac0d3j32",
"0a416d362c9e1884292c4160254a7a8afc4b3921c783114d3d7574a8087ba3da");
Assertions.assertTrue(message.contains("Sent Funds and Tokens"));
Assertions.assertTrue(message.contains("Sent Funds and Sent Tokens"));
Assertions.assertTrue(message.contains("Dexhunter Trade"));
Assertions.assertTrue(message.contains("Empowa 3,025"));
Assertions.assertTrue(message.contains("Empowa -3,025.28"));
Assertions.assertTrue(message.contains("Sent -14"));
Assertions.assertTrue(message.contains("Fee 0.25"));

// Issue #47
message = retrieveMessageByString(allMessages, "stake1u8656c05pay70xtpcwp3dqgu4jwullv6qu9e50ykn59lz7g7vzwt7",
"779133d969dc88440d18741dc17e536b8b1b21ac0fdb431f4d2850f028839d81");
Assertions.assertTrue(message.contains("Sent Funds, Sent and Received Tokens"));
Assertions.assertTrue(message.contains("Fee 0.32"));
Assertions.assertTrue(message.contains("Sent -2.00"));
Assertions.assertTrue(message.contains("iUSD -2,369.08"));
Assertions.assertTrue(message.contains("qiUSD 116,002.71"));

// check for null handles
for (String m : allMessages) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"policy_id": "1cc1aceaf5c7df55e270864a60600b9f52383fe418164574ffdeeed0",
"asset_name": "",
"asset_name_ascii": "",
"fingerprint": "asset1ajv4exyhqq8zuxvpvnqcj5vtxks0xhqvkghmwq",
"minting_tx_hash": "9d810993d5b536496dbb5c3733bf4eb6c5acdbad6bdfb6b4b84c33315de66d7e",
"total_supply": "16",
"mint_cnt": 1,
"burn_cnt": 0,
"creation_time": 1685404800,
"minting_tx_metadata": null,
"token_registry_metadata": {
"url": "",
"logo": "",
"name": "Liqwid iUSD Action",
"ticker": "iUSDAXN",
"decimals": 0,
"description": "Utility token for Liqwid iUSD market"
},
"cip68_metadata": null
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"policy_id": "416109f322b43051b80e83075b4baa8c5af14c88acaca47d5c251820",
"asset_name": "",
"asset_name_ascii": "",
"fingerprint": "asset1wjw72ah0ph7u60a7qd02xyw4mk0995wvhglwxa",
"minting_tx_hash": "9d810993d5b536496dbb5c3733bf4eb6c5acdbad6bdfb6b4b84c33315de66d7e",
"total_supply": "1",
"mint_cnt": 1,
"burn_cnt": 0,
"creation_time": 1685404800,
"minting_tx_metadata": null,
"token_registry_metadata": {
"url": "",
"logo": "",
"name": "Liqwid iUSD MarketState",
"ticker": "iUSDST",
"decimals": 0,
"description": "Utility token for Liqwid iUSD market"
},
"cip68_metadata": null
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"policy_id": "f967b3a86a9880c876851fa64b352a4d3887d6436904190b698f232e",
"asset_name": "",
"asset_name_ascii": "",
"fingerprint": "asset100qa7m200hgycvezpyyqxngz7c9606gne6etm6",
"minting_tx_hash": "9d810993d5b536496dbb5c3733bf4eb6c5acdbad6bdfb6b4b84c33315de66d7e",
"total_supply": "1",
"mint_cnt": 1,
"burn_cnt": 0,
"creation_time": 1685404800,
"minting_tx_metadata": null,
"token_registry_metadata": {
"url": "",
"logo": "",
"name": "Liqwid iUSD MarketParams",
"ticker": "iUSDPRM",
"decimals": 0,
"description": "Utility token for Liqwid iUSD market"
},
"cip68_metadata": null
}
]
Loading

0 comments on commit 71816b9

Please sign in to comment.