Skip to content

Commit

Permalink
Include new Protocol 19 Preconditions on Transaction API resource (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
sreuland authored May 4, 2022
1 parent 45a155d commit c5c9bbd
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 2 deletions.
43 changes: 42 additions & 1 deletion src/main/java/org/stellar/sdk/responses/TransactionResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.google.common.base.Optional;
import com.google.gson.annotations.SerializedName;

import lombok.Value;
import org.stellar.sdk.Memo;

import java.math.BigInteger;
Expand Down Expand Up @@ -49,6 +49,8 @@ public class TransactionResponse extends Response implements Pageable {
private List<String> signatures;
@SerializedName("fee_bump_transaction")
private FeeBumpTransaction feeBumpTransaction;
@SerializedName("preconditions")
private Preconditions preconditions;
@SerializedName("inner_transaction")
private InnerTransaction innerTransaction;
@SerializedName("account_muxed")
Expand Down Expand Up @@ -112,6 +114,10 @@ public Optional<InnerTransaction> getInner() {
return Optional.fromNullable(this.innerTransaction);
}

public Optional<Preconditions> getPreconditions() {
return Optional.fromNullable(this.preconditions);
}

public String getPagingToken() {
return pagingToken;
}
Expand Down Expand Up @@ -164,6 +170,41 @@ public Links getLinks() {
return links;
}

/**
* Preconditions of a transaction per <a href="https://github.com/stellar/stellar-protocol/blob/master/core/cap-0021.md#specification">CAP-21<a/>
*/
@Value
public static class Preconditions {
@SerializedName("timebounds")
TimeBounds timeBounds;
@SerializedName("ledgerbounds")
LedgerBounds ledgerBounds;
@SerializedName("min_account_sequence")
Long minAccountSequence;
@SerializedName("min_account_sequence_age")
long minAccountSequenceAge;
@SerializedName("min_account_sequence_ledger_gap")
long minAccountSequenceLedgerGap;
@SerializedName("extra_signers")
List<String> signatures;

@Value
public static class TimeBounds {
@SerializedName("min_time")
long minTime;
@SerializedName("max_time")
long maxTime;
}

@Value
public static class LedgerBounds {
@SerializedName("min_ledger")
long minTime;
@SerializedName("max_ledger")
long maxTime;
}
}

/**
* FeeBumpTransaction is only present in a TransactionResponse if the transaction is a fee bump transaction or is
* wrapped by a fee bump transaction. The object has two fields: the hash of the fee bump transaction and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.stellar.sdk.MemoHash;
import org.stellar.sdk.MemoNone;

import java.util.Arrays;

import static java.math.BigInteger.valueOf;

public class TransactionDeserializerTest extends TestCase {
Expand Down Expand Up @@ -61,6 +63,7 @@ public void testDeserialize() {
assertTrue(transaction.getMemo() instanceof MemoHash);
MemoHash memo = (MemoHash) transaction.getMemo();
assertEquals("51041644e83d6ac868c849418b6392ddbe9df53f000000000000000000000000", memo.getHexValue());
assertFalse(transaction.getPreconditions().isPresent());

assertEquals(transaction.getLinks().getAccount().getHref(), "/accounts/GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK");
assertEquals(transaction.getLinks().getEffects().getHref(), "/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/effects{?cursor,limit,order}");
Expand All @@ -84,13 +87,39 @@ public void testDeserializeMuxed() {
assertEquals(transaction.getFeeAccountMuxed().get().getId(), valueOf(420l));
}

@Test
@Test
public void testDeserializeWithoutMemo() {
TransactionResponse transaction = GsonSingleton.getInstance().fromJson(jsonMemoNone, TransactionResponse.class);
assertTrue(transaction.getMemo() instanceof MemoNone);
assertEquals(transaction.isSuccessful().booleanValue(), false);
}

@Test
public void testDeserializePreconditions() {
TransactionResponse transaction = GsonSingleton.getInstance().fromJson(jsonPreconditions, TransactionResponse.class);
assertTrue(transaction.getPreconditions().isPresent());
assertEquals(transaction.getPreconditions().get().getMinAccountSequence(), Long.valueOf(1));
assertEquals(transaction.getPreconditions().get().getMinAccountSequenceAge(), 2);
assertEquals(transaction.getPreconditions().get().getMinAccountSequenceLedgerGap(), 3);
assertEquals(transaction.getPreconditions().get().getTimeBounds(), new TransactionResponse.Preconditions.TimeBounds(4,5));
assertEquals(transaction.getPreconditions().get().getLedgerBounds(), new TransactionResponse.Preconditions.LedgerBounds(6,7));
assertEquals(transaction.getPreconditions().get().getSignatures(), Arrays.asList("GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK"));
}

@Test
public void testDeserializePreconditionsEmptySigners() {
TransactionResponse transaction = GsonSingleton.getInstance().fromJson(jsonPreconditionsEmptySigners, TransactionResponse.class);
assertTrue(transaction.getPreconditions().isPresent());
assertEquals(transaction.getPreconditions().get().getSignatures().size(), 0);
}

@Test
public void testDeserializePreconditionsUnsetMinAccountSequence() {
TransactionResponse transaction = GsonSingleton.getInstance().fromJson(jsonPreconditionsUnsetMinAccountSeq, TransactionResponse.class);
assertTrue(transaction.getPreconditions().isPresent());
assertNull(transaction.getPreconditions().get().getMinAccountSequence());
}

String json = "{\n" +
" \"_links\": {\n" +
" \"account\": {\n" +
Expand Down Expand Up @@ -184,6 +213,187 @@ public void testDeserializeWithoutMemo() {
" ]\n" +
"}";

String jsonPreconditions = "{\n" +
" \"_links\": {\n" +
" \"account\": {\n" +
" \"href\": \"/accounts/GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\"\n" +
" },\n" +
" \"effects\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/effects{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"ledger\": {\n" +
" \"href\": \"/ledgers/915744\"\n" +
" },\n" +
" \"operations\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/operations{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"precedes\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=asc\"\n" +
" },\n" +
" \"self\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\"\n" +
" },\n" +
" \"succeeds\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=desc\"\n" +
" }\n" +
" },\n" +
" \"id\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"paging_token\": \"3933090531512320\",\n" +
" \"successful\": false,\n" +
" \"hash\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"ledger\": 915744,\n" +
" \"created_at\": \"2015-11-20T17:01:28Z\",\n" +
" \"source_account\": \"GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\",\n" +
" \"source_account_sequence\": 2373051035426646,\n" +
" \"max_fee\": 200,\n" +
" \"fee_charged\": 100,\n" +
" \"operation_count\": 1,\n" +
" \"envelope_xdr\": \"AAAAAKgfpXwD1fWpPmZL+GkzWcBmhRQH7ouPsoTN3RoaGCfrAAAAZAAIbkcAAB9WAAAAAAAAAANRBBZE6D1qyGjISUGLY5Ldvp31PwAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAAAAAAAAADA7RnarSzCwj3OT+M2btCMFpVBdqxJS+Sr00qBjtFv7gAAAABLCs/QAAAAAAAAAAEaGCfrAAAAQG/56Cj2J8W/KCZr+oC4sWND1CTGWfaccHNtuibQH8kZIb+qBSDY94g7hiaAXrlIeg9b7oz/XuP3x9MWYw2jtwM=\",\n" +
" \"result_xdr\": \"AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=\",\n" +
" \"result_meta_xdr\": \"AAAAAAAAAAEAAAACAAAAAAAN+SAAAAAAAAAAAMDtGdqtLMLCPc5P4zZu0IwWlUF2rElL5KvTSoGO0W/uAAAAAEsKz9AADfkgAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAN+SAAAAAAAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAHp6WMr55YACD1BAAAAHgAAAAoAAAAAAAAAAAAAAAABAAAAAAAACgAAAAARC07BokpLTOF+/vVKBwiAlop7hHGJTNeGGlY4MoPykwAAAAEAAAAAK+Lzfd3yDD+Ov0GbYu1g7SaIBrKZeBUxoCunkLuI7aoAAAABAAAAAERmsKL73CyLV/HvjyQCERDXXpWE70Xhyb6MR5qPO3yQAAAAAQAAAABSORGwAdyuanN3sNOHqNSpACyYdkUM3L8VafUu69EvEgAAAAEAAAAAeCzqJNkMM/jLvyuMIfyFHljBlLCtDyj17RMycPuNtRMAAAABAAAAAIEi4R7juq15ymL00DNlAddunyFT4FyUD4muC4t3bobdAAAAAQAAAACaNpLL5YMfjOTdXVEqrAh99LM12sN6He6pHgCRAa1f1QAAAAEAAAAAqB+lfAPV9ak+Zkv4aTNZwGaFFAfui4+yhM3dGhoYJ+sAAAABAAAAAMNJrEvdMg6M+M+n4BDIdzsVSj/ZI9SvAp7mOOsvAD/WAAAAAQAAAADbHA6xiKB1+G79mVqpsHMOleOqKa5mxDpP5KEp/Xdz9wAAAAEAAAAAAAAAAA==\",\n" +
" \"memo_type\": \"none\",\n" +
" \"signatures\": [\n" +
" \"b/noKPYnxb8oJmv6gLixY0PUJMZZ9pxwc226JtAfyRkhv6oFINj3iDuGJoBeuUh6D1vujP9e4/fH0xZjDaO3Aw==\"\n" +
" ],\n" +
" \"preconditions\": {\n" +
" \"timebounds\": {\n" +
" \"min_time\": \"4\",\n" +
" \"max_time\": \"5\"\n" +
" },\n" +
" \"ledgerbounds\": {\n" +
" \"min_ledger\": 6,\n" +
" \"max_ledger\": 7\n" +
" },\n" +
" \"min_account_sequence\": \"1\",\n" +
" \"min_account_sequence_age\": \"2\",\n" +
" \"min_account_sequence_ledger_gap\": 3,\n" +
" \"extra_signers\": [\n" +
" \"GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\"\n" +
" ]\n" +
" }\n" +
"}";

String jsonPreconditionsEmptySigners = "{\n" +
" \"_links\": {\n" +
" \"account\": {\n" +
" \"href\": \"/accounts/GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\"\n" +
" },\n" +
" \"effects\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/effects{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"ledger\": {\n" +
" \"href\": \"/ledgers/915744\"\n" +
" },\n" +
" \"operations\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/operations{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"precedes\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=asc\"\n" +
" },\n" +
" \"self\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\"\n" +
" },\n" +
" \"succeeds\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=desc\"\n" +
" }\n" +
" },\n" +
" \"id\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"paging_token\": \"3933090531512320\",\n" +
" \"successful\": false,\n" +
" \"hash\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"ledger\": 915744,\n" +
" \"created_at\": \"2015-11-20T17:01:28Z\",\n" +
" \"source_account\": \"GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\",\n" +
" \"source_account_sequence\": 2373051035426646,\n" +
" \"max_fee\": 200,\n" +
" \"fee_charged\": 100,\n" +
" \"operation_count\": 1,\n" +
" \"envelope_xdr\": \"AAAAAKgfpXwD1fWpPmZL+GkzWcBmhRQH7ouPsoTN3RoaGCfrAAAAZAAIbkcAAB9WAAAAAAAAAANRBBZE6D1qyGjISUGLY5Ldvp31PwAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAAAAAAAAADA7RnarSzCwj3OT+M2btCMFpVBdqxJS+Sr00qBjtFv7gAAAABLCs/QAAAAAAAAAAEaGCfrAAAAQG/56Cj2J8W/KCZr+oC4sWND1CTGWfaccHNtuibQH8kZIb+qBSDY94g7hiaAXrlIeg9b7oz/XuP3x9MWYw2jtwM=\",\n" +
" \"result_xdr\": \"AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=\",\n" +
" \"result_meta_xdr\": \"AAAAAAAAAAEAAAACAAAAAAAN+SAAAAAAAAAAAMDtGdqtLMLCPc5P4zZu0IwWlUF2rElL5KvTSoGO0W/uAAAAAEsKz9AADfkgAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAN+SAAAAAAAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAHp6WMr55YACD1BAAAAHgAAAAoAAAAAAAAAAAAAAAABAAAAAAAACgAAAAARC07BokpLTOF+/vVKBwiAlop7hHGJTNeGGlY4MoPykwAAAAEAAAAAK+Lzfd3yDD+Ov0GbYu1g7SaIBrKZeBUxoCunkLuI7aoAAAABAAAAAERmsKL73CyLV/HvjyQCERDXXpWE70Xhyb6MR5qPO3yQAAAAAQAAAABSORGwAdyuanN3sNOHqNSpACyYdkUM3L8VafUu69EvEgAAAAEAAAAAeCzqJNkMM/jLvyuMIfyFHljBlLCtDyj17RMycPuNtRMAAAABAAAAAIEi4R7juq15ymL00DNlAddunyFT4FyUD4muC4t3bobdAAAAAQAAAACaNpLL5YMfjOTdXVEqrAh99LM12sN6He6pHgCRAa1f1QAAAAEAAAAAqB+lfAPV9ak+Zkv4aTNZwGaFFAfui4+yhM3dGhoYJ+sAAAABAAAAAMNJrEvdMg6M+M+n4BDIdzsVSj/ZI9SvAp7mOOsvAD/WAAAAAQAAAADbHA6xiKB1+G79mVqpsHMOleOqKa5mxDpP5KEp/Xdz9wAAAAEAAAAAAAAAAA==\",\n" +
" \"memo_type\": \"none\",\n" +
" \"signatures\": [\n" +
" \"b/noKPYnxb8oJmv6gLixY0PUJMZZ9pxwc226JtAfyRkhv6oFINj3iDuGJoBeuUh6D1vujP9e4/fH0xZjDaO3Aw==\"\n" +
" ],\n" +
" \"preconditions\": {\n" +
" \"timebounds\": {\n" +
" \"min_time\": \"4\",\n" +
" \"max_time\": \"5\"\n" +
" },\n" +
" \"ledgerbounds\": {\n" +
" \"min_ledger\": 6,\n" +
" \"max_ledger\": 7\n" +
" },\n" +
" \"min_account_sequence\": \"1\",\n" +
" \"min_account_sequence_age\": \"2\",\n" +
" \"min_account_sequence_ledger_gap\": 3,\n" +
" \"extra_signers\": [\n]\n" +
" }\n" +
"}";

String jsonPreconditionsUnsetMinAccountSeq = "{\n" +
" \"_links\": {\n" +
" \"account\": {\n" +
" \"href\": \"/accounts/GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\"\n" +
" },\n" +
" \"effects\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/effects{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"ledger\": {\n" +
" \"href\": \"/ledgers/915744\"\n" +
" },\n" +
" \"operations\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b/operations{?cursor,limit,order}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"precedes\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=asc\"\n" +
" },\n" +
" \"self\": {\n" +
" \"href\": \"/transactions/5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\"\n" +
" },\n" +
" \"succeeds\": {\n" +
" \"href\": \"/transactions?cursor=3933090531512320\\u0026order=desc\"\n" +
" }\n" +
" },\n" +
" \"id\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"paging_token\": \"3933090531512320\",\n" +
" \"successful\": false,\n" +
" \"hash\": \"5c2e4dad596941ef944d72741c8f8f1a4282f8f2f141e81d827f44bf365d626b\",\n" +
" \"ledger\": 915744,\n" +
" \"created_at\": \"2015-11-20T17:01:28Z\",\n" +
" \"source_account\": \"GCUB7JL4APK7LKJ6MZF7Q2JTLHAGNBIUA7XIXD5SQTG52GQ2DAT6XZMK\",\n" +
" \"source_account_sequence\": 2373051035426646,\n" +
" \"max_fee\": 200,\n" +
" \"fee_charged\": 100,\n" +
" \"operation_count\": 1,\n" +
" \"envelope_xdr\": \"AAAAAKgfpXwD1fWpPmZL+GkzWcBmhRQH7ouPsoTN3RoaGCfrAAAAZAAIbkcAAB9WAAAAAAAAAANRBBZE6D1qyGjISUGLY5Ldvp31PwAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAAAAAAAAADA7RnarSzCwj3OT+M2btCMFpVBdqxJS+Sr00qBjtFv7gAAAABLCs/QAAAAAAAAAAEaGCfrAAAAQG/56Cj2J8W/KCZr+oC4sWND1CTGWfaccHNtuibQH8kZIb+qBSDY94g7hiaAXrlIeg9b7oz/XuP3x9MWYw2jtwM=\",\n" +
" \"result_xdr\": \"AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=\",\n" +
" \"result_meta_xdr\": \"AAAAAAAAAAEAAAACAAAAAAAN+SAAAAAAAAAAAMDtGdqtLMLCPc5P4zZu0IwWlUF2rElL5KvTSoGO0W/uAAAAAEsKz9AADfkgAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAN+SAAAAAAAAAAAP1qe44j+i4uIT+arbD4QDQBt8ryEeJd7a0jskQ3nwDeAAHp6WMr55YACD1BAAAAHgAAAAoAAAAAAAAAAAAAAAABAAAAAAAACgAAAAARC07BokpLTOF+/vVKBwiAlop7hHGJTNeGGlY4MoPykwAAAAEAAAAAK+Lzfd3yDD+Ov0GbYu1g7SaIBrKZeBUxoCunkLuI7aoAAAABAAAAAERmsKL73CyLV/HvjyQCERDXXpWE70Xhyb6MR5qPO3yQAAAAAQAAAABSORGwAdyuanN3sNOHqNSpACyYdkUM3L8VafUu69EvEgAAAAEAAAAAeCzqJNkMM/jLvyuMIfyFHljBlLCtDyj17RMycPuNtRMAAAABAAAAAIEi4R7juq15ymL00DNlAddunyFT4FyUD4muC4t3bobdAAAAAQAAAACaNpLL5YMfjOTdXVEqrAh99LM12sN6He6pHgCRAa1f1QAAAAEAAAAAqB+lfAPV9ak+Zkv4aTNZwGaFFAfui4+yhM3dGhoYJ+sAAAABAAAAAMNJrEvdMg6M+M+n4BDIdzsVSj/ZI9SvAp7mOOsvAD/WAAAAAQAAAADbHA6xiKB1+G79mVqpsHMOleOqKa5mxDpP5KEp/Xdz9wAAAAEAAAAAAAAAAA==\",\n" +
" \"memo_type\": \"none\",\n" +
" \"signatures\": [\n" +
" \"b/noKPYnxb8oJmv6gLixY0PUJMZZ9pxwc226JtAfyRkhv6oFINj3iDuGJoBeuUh6D1vujP9e4/fH0xZjDaO3Aw==\"\n" +
" ],\n" +
" \"preconditions\": {\n" +
" \"timebounds\": {\n" +
" \"min_time\": \"4\",\n" +
" \"max_time\": \"5\"\n" +
" },\n" +
" \"ledgerbounds\": {\n" +
" \"min_ledger\": 6,\n" +
" \"max_ledger\": 7\n" +
" },\n" +
" \"min_account_sequence_age\": \"2\",\n" +
" \"min_account_sequence_ledger_gap\": 3,\n" +
" \"extra_signers\": [\n]\n" +
" }\n" +
"}";

String jsonFeeBump = "{\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
Expand Down

0 comments on commit c5c9bbd

Please sign in to comment.