forked from Qortal/qortal
Interim commit with newer asset order "price" arg
+ unit test + newer unit test harness but still needs: BlockChain config support for activating newer "price" arg New unit test to check old "price" arg usage Rework existing asset-related unit tests Check API inputs/output pre/post "price" arg crossover
This commit is contained in:
parent
d3c1602d9b
commit
789b311984
@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.account.Account;
|
||||
import org.qora.account.PublicKeyAccount;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.data.asset.AssetData;
|
||||
import org.qora.data.asset.OrderData;
|
||||
import org.qora.data.asset.TradeData;
|
||||
@ -110,10 +111,16 @@ public class Order {
|
||||
// Save this order into repository so it's available for matching, possibly by itself
|
||||
this.repository.getAssetRepository().save(this.orderData);
|
||||
|
||||
boolean isOurOrderV2 = this.orderData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp();
|
||||
|
||||
// Attempt to match orders
|
||||
LOGGER.debug("Processing our order " + HashCode.fromBytes(this.orderData.getOrderId()).toString());
|
||||
LOGGER.trace("We have: " + this.orderData.getAmount().toPlainString() + " " + haveAssetData.getName());
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
|
||||
if (isOurOrderV2)
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName());
|
||||
else
|
||||
LOGGER.trace("We want " + this.orderData.getPrice().toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
|
||||
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetId args.
|
||||
// Returned orders are sorted with lowest "price" first.
|
||||
@ -121,7 +128,7 @@ public class Order {
|
||||
LOGGER.trace("Open orders fetched from repository: " + orders.size());
|
||||
|
||||
/*
|
||||
* Our order example:
|
||||
* Our order example (V1):
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=0.002
|
||||
*
|
||||
@ -130,17 +137,38 @@ public class Order {
|
||||
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
|
||||
*
|
||||
* So 500 GOLD [each] is our "buyingPrice".
|
||||
*
|
||||
* Our order example (V2):
|
||||
*
|
||||
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), price=20
|
||||
*
|
||||
* This translates to "we have 10,000 GOLD and want to buy 20 QORA"
|
||||
*
|
||||
* So if our order matched, we'd end up with 20 QORA, essentially costing 10,000 / 20 = 500 GOLD each.
|
||||
*
|
||||
* So 500 GOLD [each] is our "buyingPrice".
|
||||
*/
|
||||
BigDecimal ourPrice = this.orderData.getPrice();
|
||||
BigDecimal ourAmount = this.orderData.getAmount();
|
||||
BigDecimal ourPrice;
|
||||
if (isOurOrderV2)
|
||||
ourPrice = ourAmount.divide(this.orderData.getPrice(), RoundingMode.DOWN);
|
||||
else
|
||||
ourPrice = this.orderData.getPrice();
|
||||
|
||||
for (OrderData theirOrderData : orders) {
|
||||
LOGGER.trace("Considering order " + HashCode.fromBytes(theirOrderData.getOrderId()).toString());
|
||||
// Note swapped use of have/want asset data as this is from 'their' perspective.
|
||||
LOGGER.trace("They have: " + theirOrderData.getAmount().toPlainString() + " " + wantAssetData.getName());
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName() + " per " + wantAssetData.getName());
|
||||
|
||||
boolean isTheirOrderV2 = theirOrderData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp();
|
||||
|
||||
if (isTheirOrderV2)
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName());
|
||||
else
|
||||
LOGGER.trace("They want " + theirOrderData.getPrice().toPlainString() + " " + haveAssetData.getName() + " per " + wantAssetData.getName());
|
||||
|
||||
/*
|
||||
* Potential matching order example:
|
||||
* Potential matching order example (V1):
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
|
||||
*
|
||||
@ -149,10 +177,25 @@ public class Order {
|
||||
* So if their order matched, they'd end up with 40 * 486 = 19,440 GOLD, essentially costing 1/486 = 0.00205761 QORA each.
|
||||
*
|
||||
* So 0.00205761 QORA [each] is their "buyingPrice".
|
||||
*
|
||||
* Potential matching order example (V2):
|
||||
*
|
||||
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=19,440
|
||||
*
|
||||
* This translates to "we have 40 QORA and want to buy 19,440 GOLD"
|
||||
*
|
||||
* So if their order matched, they'd end up with 19,440 GOLD, essentially costing 40 / 19,440 = 0.00205761 QORA each.
|
||||
*
|
||||
* So 0.00205761 QORA [each] is their "buyingPrice".
|
||||
*/
|
||||
|
||||
// Round down otherwise their buyingPrice would be better than advertised and cause issues
|
||||
BigDecimal theirBuyingPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
BigDecimal theirBuyingPrice;
|
||||
|
||||
if (isTheirOrderV2)
|
||||
theirBuyingPrice = theirOrderData.getAmount().divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
else
|
||||
theirBuyingPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
|
||||
LOGGER.trace("theirBuyingPrice: " + theirBuyingPrice.toPlainString() + " " + wantAssetData.getName() + " per " + haveAssetData.getName());
|
||||
|
||||
// If their buyingPrice is less than what we're willing to pay then we're done as prices only get worse as we iterate through list of orders
|
||||
@ -162,9 +205,11 @@ public class Order {
|
||||
// Calculate how many want-asset we could buy at their price
|
||||
BigDecimal ourAmountLeft = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
|
||||
LOGGER.trace("ourAmountLeft (max we could buy at their price): " + ourAmountLeft.toPlainString() + " " + wantAssetData.getName());
|
||||
|
||||
// How many want-asset is remaining available in this order
|
||||
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
|
||||
LOGGER.trace("theirAmountLeft (max amount remaining in order): " + theirAmountLeft.toPlainString() + " " + wantAssetData.getName());
|
||||
|
||||
// So matchable want-asset amount is the minimum of above two values
|
||||
BigDecimal matchedAmount = ourAmountLeft.min(theirAmountLeft);
|
||||
LOGGER.trace("matchedAmount: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
|
||||
@ -186,7 +231,11 @@ public class Order {
|
||||
// Trade can go ahead!
|
||||
|
||||
// Calculate the total cost to us, in have-asset, based on their price
|
||||
BigDecimal tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
|
||||
BigDecimal tradePrice;
|
||||
if (isTheirOrderV2)
|
||||
tradePrice = matchedAmount.divide(theirBuyingPrice).setScale(8); // XXX is this right?
|
||||
else
|
||||
tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
|
||||
LOGGER.trace("tradePrice ('want' trade agreed): " + tradePrice.toPlainString() + " " + haveAssetData.getName());
|
||||
|
||||
// Construct trade
|
||||
|
@ -310,6 +310,7 @@ public class GenesisBlock extends Block {
|
||||
}
|
||||
|
||||
transaction.process();
|
||||
creator.setLastReference(transactionData.getSignature());
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
throw new RuntimeException("Can't process genesis block transaction", e);
|
||||
|
@ -22,7 +22,7 @@ public class CreateAssetOrderTransactionData extends TransactionData {
|
||||
private long wantAssetId;
|
||||
@Schema(description = "amount of \"have\" asset to trade")
|
||||
private BigDecimal amount;
|
||||
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
|
||||
@Schema(description = "amount of \"want\" asset to receive")
|
||||
private BigDecimal price;
|
||||
|
||||
// Constructors
|
||||
|
@ -130,9 +130,16 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
return ValidationResult.INVALID_AMOUNT;
|
||||
|
||||
// Check total return from fulfilled order would be integer if "want" asset is not divisible
|
||||
if (!wantAssetData.getIsDivisible()
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getQoraV2Timestamp()) {
|
||||
// v2
|
||||
if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getPrice().stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
} else {
|
||||
// v1
|
||||
if (!wantAssetData.getIsDivisible()
|
||||
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice()).stripTrailingZeros().scale() > 0)
|
||||
return ValidationResult.INVALID_RETURN;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
@ -1,24 +1,38 @@
|
||||
package org.qora.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.net.URL;
|
||||
import java.security.Security;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.block.BlockGenerator;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryFactory;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qora.settings.Settings;
|
||||
import org.qora.test.common.TestAccount;
|
||||
import org.qora.transaction.Transaction;
|
||||
import org.qora.transaction.Transaction.ValidationResult;
|
||||
|
||||
public class Common {
|
||||
|
||||
public static final String testConnectionUrl = "jdbc:hsqldb:mem:testdb";
|
||||
// public static final String testConnectionUrl = "jdbc:hsqldb:file:testdb/blockchain;create=true";
|
||||
|
||||
public static final String testSettingsFilename = "test-settings.json";
|
||||
|
||||
public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
@ -33,7 +47,53 @@ public class Common {
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
Settings.fileInstance(testSettingsFilename);
|
||||
URL testSettingsUrl = Common.class.getClassLoader().getResource(testSettingsFilename);
|
||||
assertNotNull("Test settings JSON file not found", testSettingsUrl);
|
||||
Settings.fileInstance(testSettingsUrl.getPath());
|
||||
}
|
||||
|
||||
public static Map<String, TransactionData> lastTransactionByAddress;
|
||||
public static Map<String, TestAccount> testAccountsByName = new HashMap<>();
|
||||
static {
|
||||
testAccountsByName.put("main", new TestAccount("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"));
|
||||
testAccountsByName.put("dummy", new TestAccount("AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot"));
|
||||
}
|
||||
|
||||
public static PrivateKeyAccount getTestAccount(Repository repository, String name) {
|
||||
return new PrivateKeyAccount(repository, testAccountsByName.get(name).getSeed());
|
||||
}
|
||||
|
||||
public static void resetBlockchain() throws DataException {
|
||||
BlockChain.validate();
|
||||
lastTransactionByAddress = new HashMap<>();
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
for (TestAccount account : testAccountsByName.values()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, account.getAddress(), ConfirmationStatus.BOTH, 1, null, true);
|
||||
assertFalse("Test account should have existing transaction", signatures.isEmpty());
|
||||
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signatures.get(0));
|
||||
lastTransactionByAddress.put(account.getAddress(), transactionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(signingAccount);
|
||||
|
||||
// Add to unconfirmed
|
||||
assertTrue("Transaction's signature should be valid", transaction.isSignatureValid());
|
||||
|
||||
ValidationResult result = transaction.isValidUnconfirmed();
|
||||
assertEquals("Transaction invalid", ValidationResult.OK, result);
|
||||
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
repository.getTransactionRepository().unconfirmTransaction(transactionData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Generate block
|
||||
BlockGenerator.generateTestingBlock(repository, signingAccount);
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
@ -47,7 +107,7 @@ public class Common {
|
||||
RepositoryManager.closeRepositoryFactory();
|
||||
}
|
||||
|
||||
public static void assetEmptyBlockchain(Repository repository) throws DataException {
|
||||
public static void assertEmptyBlockchain(Repository repository) throws DataException {
|
||||
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
|
||||
}
|
||||
|
||||
|
54
src/test/java/org/qora/test/assets/TradingTests.java
Normal file
54
src/test/java/org/qora/test/assets/TradingTests.java
Normal file
@ -0,0 +1,54 @@
|
||||
package org.qora.test.assets;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.Common;
|
||||
import org.qora.test.common.AssetUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class TradingTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.resetBlockchain();
|
||||
}
|
||||
|
||||
/*
|
||||
* Check full matching of orders with prices that
|
||||
* can't be represented in floating binary.
|
||||
*
|
||||
* For example, sell 1 GOLD for 12 QORA so
|
||||
* price is 1/12 or 0.083...
|
||||
*/
|
||||
@Test
|
||||
public void testNonExactFraction() throws DataException {
|
||||
final long qoraAmount = 24L;
|
||||
final long otherAmount = 2L;
|
||||
|
||||
final long transferAmount = 100L;
|
||||
|
||||
try (Repository repository = RepositoryManager.getRepository()) {
|
||||
// Create initial order
|
||||
AssetUtils.createOrder(repository, "main", Asset.QORA, AssetUtils.testAssetId, qoraAmount, otherAmount);
|
||||
|
||||
// Give 100 asset to other account so they can create order
|
||||
AssetUtils.transferAsset(repository, "main", "dummy", AssetUtils.testAssetId, transferAmount);
|
||||
|
||||
// Create matching order
|
||||
AssetUtils.createOrder(repository, "dummy", AssetUtils.testAssetId, Asset.QORA, otherAmount, qoraAmount);
|
||||
|
||||
// Check balances to check expected outcome
|
||||
BigDecimal actualAmount = Common.getTestAccount(repository, "dummy").getConfirmedBalance(AssetUtils.testAssetId);
|
||||
BigDecimal expectedAmount = BigDecimal.valueOf(transferAmount - otherAmount).setScale(8);
|
||||
assertTrue("dummy account's asset balance incorrect", actualAmount.compareTo(expectedAmount) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
46
src/test/java/org/qora/test/common/AssetUtils.java
Normal file
46
src/test/java/org/qora/test/common/AssetUtils.java
Normal file
@ -0,0 +1,46 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.data.transaction.CreateAssetOrderTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.data.transaction.TransferAssetTransactionData;
|
||||
import org.qora.group.Group;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.test.Common;
|
||||
|
||||
public class AssetUtils {
|
||||
|
||||
public static final int txGroupId = Group.NO_GROUP;
|
||||
public static final BigDecimal fee = BigDecimal.ONE.setScale(8);
|
||||
public static final long testAssetId = 1L;
|
||||
|
||||
public static void transferAsset(Repository repository, String fromAccountName, String toAccountName, long assetId, long amount) throws DataException {
|
||||
PrivateKeyAccount fromAccount = Common.getTestAccount(repository, fromAccountName);
|
||||
PrivateKeyAccount toAccount = Common.getTestAccount(repository, toAccountName);
|
||||
|
||||
byte[] reference = fromAccount.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
|
||||
|
||||
TransactionData transferAssetTransactionData = new TransferAssetTransactionData(timestamp, AssetUtils.txGroupId, reference, fromAccount.getPublicKey(), toAccount.getAddress(), BigDecimal.valueOf(amount), assetId, AssetUtils.fee);
|
||||
|
||||
Common.signAndForge(repository, transferAssetTransactionData, fromAccount);
|
||||
}
|
||||
|
||||
public static void createOrder(Repository repository, String accountName, long haveAssetId, long wantAssetId, long haveAmount, long wantAmount) throws DataException {
|
||||
PrivateKeyAccount account = Common.getTestAccount(repository, accountName);
|
||||
|
||||
byte[] reference = account.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1000;
|
||||
BigDecimal amount = BigDecimal.valueOf(haveAmount);
|
||||
BigDecimal price = BigDecimal.valueOf(wantAmount);
|
||||
|
||||
// Note: "price" is not the same in V2 as in V1
|
||||
TransactionData initialOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee);
|
||||
|
||||
Common.signAndForge(repository, initialOrderTransactionData, account);
|
||||
}
|
||||
|
||||
}
|
10
src/test/java/org/qora/test/common/TestAccount.java
Normal file
10
src/test/java/org/qora/test/common/TestAccount.java
Normal file
@ -0,0 +1,10 @@
|
||||
package org.qora.test.common;
|
||||
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.utils.Base58;
|
||||
|
||||
public class TestAccount extends PrivateKeyAccount {
|
||||
public TestAccount(String privateKey) {
|
||||
super(null, Base58.decode(privateKey));
|
||||
}
|
||||
}
|
@ -7,16 +7,17 @@
|
||||
"blockTimestampMargin": 500,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"requireGroupForApproval": true,
|
||||
"defaultGroupId": 2,
|
||||
"requireGroupForApproval": false,
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
"timestamp": 0,
|
||||
"generatingBalance": "10000000",
|
||||
"transactions": [
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "data": "", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" },
|
||||
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "9876543210.12345678", "fee": 0 },
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }
|
||||
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000", "fee": 0 },
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "test", "description": "test asset", "data": "", "quantity": 1000, "isDivisible": true, "fee": 0 }
|
||||
]
|
||||
},
|
||||
"featureTriggers": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user