All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ergoplatform.appkit.BoxOperations Maven / Gradle / Ivy

The newest version!
package org.ergoplatform.appkit;

import static com.google.common.base.Preconditions.checkState;
import static org.ergoplatform.appkit.BlockchainContext.DEFAULT_LIMIT_FOR_API;
import static org.ergoplatform.appkit.Parameters.MinFee;

import com.google.common.base.Preconditions;
import org.ergoplatform.sdk.ErgoToken;
import org.ergoplatform.sdk.SecretString;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * A collection of utility operations implemented in terms of abstract Appkit interfaces.
 */
public class BoxOperations {
    private final BlockchainContext ctx;
    private final List
senders; private final ErgoProver senderProver; private long amountToSpend = 0; private List tokensToSpend = Collections.emptyList(); private long feeAmount = MinFee; private IUnspentBoxesLoader inputBoxesLoader = new ExplorerApiUnspentLoader(); private BoxAttachment attachment; private int maxInputBoxesToSelect = 0; private static final long CHANGE_BOX_NANOERG = MinFee; BoxOperations(BlockchainContext ctx, List
senders, @Nullable ErgoProver senderProver) { this.ctx = ctx; this.senders = senders; this.senderProver = senderProver; } /** * Construct BoxOperations with a single sender address * * @param sender sender the following methods should use */ public static BoxOperations createForSender(Address sender, BlockchainContext ctx) { return createForSenders(Collections.singletonList(sender), ctx); } /** * Construct BoxOperations with a list of sender addresses * * @param senders list of senders the following methods should use */ public static BoxOperations createForSenders(List
senders, BlockchainContext ctx) { return new BoxOperations(ctx, senders, null); } /** * Construct BoxOperations with a prover, deriving list of senders from prover from the * EIP-3 addresses. * All the derived EIP-3 addresses of the prover can be used to collect unspent boxes. */ public static BoxOperations createForEip3Prover(ErgoProver senderProver, BlockchainContext ctx) { List
eip3Addresses = senderProver.getEip3Addresses(); checkState(eip3Addresses.size() > 0, "EIP-3 addresses are not derived in the prover (use ErgoProverBuilder.withEip3Secret)"); return new BoxOperations(ctx, eip3Addresses, senderProver); } /** * Construct BoxOperations with a prover, deriving list of senders from prover from either the * MASTER address of the given prover. */ public static BoxOperations createForProver(ErgoProver senderProver, BlockchainContext ctx) { return new BoxOperations(ctx, Collections.singletonList(senderProver.getAddress()), senderProver); } /** * @param amountToSpend nanoerg value to be collected in inboxes */ public BoxOperations withAmountToSpend(long amountToSpend) { if (amountToSpend < 0) { throw new IllegalArgumentException("Amount to send must be >= 0"); } this.amountToSpend = amountToSpend; return this; } /** * @param tokensToSpend tokens to be collected in inboxes */ public BoxOperations withTokensToSpend(@Nonnull List tokensToSpend) { this.tokensToSpend = tokensToSpend; return this; } /** * @param feeAmount fee amount in nanoerg to be used for generated transactions */ public BoxOperations withFeeAmount(long feeAmount) { if (feeAmount < MinFee) { throw new IllegalArgumentException("Amount to send must be >= " + MinFee); } this.feeAmount = feeAmount; return this; } /** * @param attachment attachment to be set for outboxes */ public BoxOperations withAttachment(@Nullable BoxAttachment attachment) { this.attachment = attachment; return this; } public int getMaxInputBoxesToSelect() { return maxInputBoxesToSelect; } /** * @param maxInputBoxesToSelect if set greater than 0, {@link #loadTop()} will only select * up to this number of input boxes to satisfy ERG and token amount * needed and throws an {@link org.ergoplatform.appkit.InputBoxesSelectionException.InputBoxLimitExceededException} * otherwise * if set to <= 0 (or not set), there is no input box restriction * checked by loadTop. */ public BoxOperations withMaxInputBoxesToSelect(int maxInputBoxesToSelect) { this.maxInputBoxesToSelect = maxInputBoxesToSelect; return this; } /** * @param message message to be set for outboxes as {@link BoxAttachmentPlainText} */ public BoxOperations withMessage(@Nullable String message) { if (message != null) { withAttachment(BoxAttachmentPlainText.buildForText(message)); } else { withAttachment(null); } return this; } /** * @param inputBoxesSource to use for {@link #getCoveringBoxesFor(long, List, boolean, Function)} * See {@link IUnspentBoxesLoader for more information} * Default is {@link ExplorerApiUnspentLoader} */ public BoxOperations withInputBoxesLoader(@Nonnull IUnspentBoxesLoader inputBoxesSource) { this.inputBoxesLoader = inputBoxesSource; return this; } /** * @return context this class was created with */ public BlockchainContext getBlockchainContext() { return ctx; } /** * @return senders this class was created with */ public List
getSenders() { return senders; } /** * @return currently set amount to spend */ public long getAmountToSpend() { return amountToSpend; } /** * @return currently set tokens to spend */ public List getTokensToSpend() { return tokensToSpend; } /** * @return currently set fee amount */ public long getFeeAmount() { return feeAmount; } @Deprecated public static ErgoProver createProver(BlockchainContext ctx, Mnemonic mnemonic, Boolean usePre1627KeyDerivation) { ErgoProver prover = ctx.newProverBuilder() .withMnemonic(mnemonic.getPhrase(), mnemonic.getPassword(), usePre1627KeyDerivation) .build(); return prover; } @Deprecated public static ErgoProverBuilder createProver(BlockchainContext ctx, String storageFile, SecretString storagePass) { return createProver(ctx, storageFile, storagePass.toStringUnsecure()); } @Deprecated public static ErgoProverBuilder createProver( BlockchainContext ctx, String storageFile, String storagePass) { SecretStorage storage = SecretStorage.loadFrom(storageFile); storage.unlock(storagePass); ErgoProverBuilder proverB = ctx.newProverBuilder() .withSecretStorage(storage); return proverB; } /** * Send the specified amountToSpend and tokens to the recipient. * * @param recipient the recipient address * @return json of the signed transaction */ public String send(Address recipient) { ErgoContract contract = recipient.toErgoContract(); SignedTransaction signed = putToContractTx(contract); ctx.sendTransaction(signed); return signed.toJson(true); } /** * Load boxes for the given sender addresses covering the given amount of NanoErgs, fee and tokens. * The given page of boxes is loaded from each address and concatenated to a single * list. * The list is then used to select covering boxes. * * The method respects a max amount of boxes to be selected set by {@link #withMaxInputBoxesToSelect(int)}. * If this limit is exceeded, a {@link org.ergoplatform.appkit.InputBoxesSelectionException.InputBoxLimitExceededException} * is thrown and no further boxes are loaded. * * @return a list of boxes covering the given amount */ public List loadTop() { return loadTop(0); } /** * Like {@link #loadTop()} loading and returning unspent boxes covering the given amount of * nanoergs, fee and tokens, but you can specify an amount of nanoergs that is already covered * by other input boxes and does not need to be satisfied. * * @param amountCovered nanoerg amount that is assumed to be covered by input boxes you provide */ public List loadTop(long amountCovered) { List unspentBoxes = new ArrayList<>(); long grossAmount = amountToSpend + feeAmount - amountCovered; long remainingAmount = grossAmount; boolean changeBoxConsidered = false; SelectTokensHelper tokensHelper = new SelectTokensHelper(tokensToSpend); List remainingTokens = tokensToSpend; inputBoxesLoader.prepare(ctx, senders, grossAmount, tokensToSpend); for (Address sender : senders) { inputBoxesLoader.prepareForAddress(sender); CoveringBoxes addressUnspentBoxes = getCoveringBoxesFor(remainingAmount, remainingTokens, changeBoxConsidered, page -> inputBoxesLoader.loadBoxesPage(ctx, sender, page), (maxInputBoxesToSelect <= 0) ? 0 : Math.max(1, maxInputBoxesToSelect - unspentBoxes.size()) ); // when a change box needed it needs some extra nanoergs to be sent if (!changeBoxConsidered && addressUnspentBoxes.isChangeBoxNeeded()) { changeBoxConsidered = true; remainingAmount = remainingAmount + CHANGE_BOX_NANOERG; } for (InputBox b : addressUnspentBoxes.getBoxes()) { unspentBoxes.add(b); tokensHelper.useTokens(b.getTokens()); remainingAmount -= b.getValue(); if (remainingAmount <= 0 && tokensHelper.areTokensCovered()) { // collected enough boxes to cover the amount break; } } if (remainingAmount <= 0 && tokensHelper.areTokensCovered()) break; remainingTokens = tokensHelper.getRemainingTokenList(); } // check if we have enough tokens and ERG InputBoxesValidatorJavaHelper.validateBoxes(unspentBoxes, grossAmount, tokensToSpend); return unspentBoxes; } /** * Creates a new {@link SignedTransaction} which sends the given amount of NanoErgs and tokens * to the given contract. */ public SignedTransaction putToContractTx( ErgoContract contract) { if (senderProver == null) { throw new IllegalStateException("Call this only when prover is set"); } UnsignedTransaction tx = putToContractTxUnsigned(contract); SignedTransaction signed = senderProver.sign(tx); return signed; } /** * Creates a new {@link UnsignedTransaction} which sends the given amount of NanoErgs and tokens * to the given contract. */ public UnsignedTransaction putToContractTxUnsigned( ErgoContract contract) { return buildTxWithDefaultInputs(txB -> { OutBoxBuilder outBoxBuilder = prepareOutBox(txB) .contract(contract); OutBox newBox = outBoxBuilder.build(); txB.outputs(newBox); return txB; }); } /** * @return OutBoxBuilder prepared with the properties set to this BoxOperations instance: tokens to * spend, amount to spend and attachment. */ public OutBoxBuilder prepareOutBox(UnsignedTransactionBuilder txB) { OutBoxBuilder outBoxBuilder = txB.outBoxBuilder() .value(amountToSpend); if (!tokensToSpend.isEmpty()) outBoxBuilder.tokens(tokensToSpend.toArray(new ErgoToken[]{})); if (attachment != null) { outBoxBuilder.registers(attachment.getOutboxRegistersForAttachment()); } return outBoxBuilder; } /** * Creates a new {@link UnsignedTransaction} which sends the given amount of NanoErgs and a * newly minted token to the given contract. * * @param contract contract to send the newly minted token to * @param tokenBuilder receives the id of the token to mint, must return the new token * @return unsigned transaction */ public UnsignedTransaction mintTokenToContractTxUnsigned(ErgoContract contract, Function tokenBuilder) { if (!tokensToSpend.isEmpty()) { throw new IllegalArgumentException("Mint token not possible with spending tokens"); } if (attachment != null) { throw new IllegalArgumentException("Mint token not possible with attachment"); } return buildTxWithDefaultInputs(txB -> { OutBox newBox = txB.outBoxBuilder() .value(amountToSpend) .contract(contract) .mintToken(tokenBuilder.apply(txB.getInputBoxes().get(0).getId().toString())) .build(); txB.outputs(newBox); return txB; }); } /** * Creates a new {@link UnsignedTransaction} preparing inputs, fee and change address. * The given outputBuilder is used to prepare and add outboxes to the resulting transaction. * * See {@link #putToContractTxUnsigned(ErgoContract)} how to use. */ public UnsignedTransaction buildTxWithDefaultInputs(Function outputBuilder) { List boxesToSpend = loadTop(); UnsignedTransactionBuilder txB = ctx.newTxBuilder(); UnsignedTransactionBuilder unsignedTransactionBuilder = txB.boxesToSpend(boxesToSpend) .fee(feeAmount) .sendChangeTo(senders.get(0).getErgoAddress()); return outputBuilder.apply(unsignedTransactionBuilder).build(); } public static SignedTransaction spendBoxesTx( BlockchainContext ctx, UnsignedTransactionBuilder txB, List boxes, ErgoProver sender, Address recipient, long amount, long fee) { OutBox newBox = txB.outBoxBuilder() .value(amount) .contract(recipient.toErgoContract()) .build(); UnsignedTransaction tx = txB.boxesToSpend(boxes) .addOutputs(newBox) .fee(fee) .sendChangeTo(sender.getP2PKAddress()) .build(); SignedTransaction signed = sender.sign(tx); return signed; } /** * Get unspent boxes from a paged boxes source and selects the top ones needed to spent * to satisfy amountToSpend and tokensToSpend. *

* inputBoxesLoader must satisfy the following needs: * - receives a 0-based integer, the page that should be loaded * - returns a list of {@link InputBox} to select from. First items are preferred to be selected * - must not return null * - returning an empty list means the source of input boxes is drained and no further page will * be loaded * * @param amountToSpend amount of NanoErgs to be covered * @param tokensToSpend ErgoToken to spent * @param changeBoxConsidered true if CHANGE_BOX_NANOERG amount for a change box is already * included in amountToSpend and does not need to be added any more * @param inputBoxesLoader method returning paged sets of InputBoxes, see above * @return a new instance of {@link CoveringBoxes} set */ public static CoveringBoxes getCoveringBoxesFor(long amountToSpend, List tokensToSpend, boolean changeBoxConsidered, Function> inputBoxesLoader) { return getCoveringBoxesFor(amountToSpend, tokensToSpend, changeBoxConsidered, inputBoxesLoader, 0); } private static CoveringBoxes getCoveringBoxesFor(long amountToSpend, List tokensToSpend, boolean changeBoxConsidered, Function> inputBoxesLoader, int maxBoxesToSelect) { SelectTokensHelper tokensRemaining = new SelectTokensHelper(tokensToSpend); Preconditions.checkArgument(amountToSpend > 0 || !tokensRemaining.areTokensCovered(), "amountToSpend or tokens to spend should be > 0"); ArrayList selectedCoveringBoxes = new ArrayList<>(); long remainingAmountToCover = amountToSpend; int page = 0; while (true) { List chunk = inputBoxesLoader.apply(page); for (InputBox boxCandidate : chunk) { // on rare occasions, chunk can include entries that we already had received on a // previous chunk page. We make sure we don't add any duplicate entries. if (!isAlreadyAdded(selectedCoveringBoxes, boxCandidate)) { boolean usefulTokens = tokensRemaining.areTokensNeeded(boxCandidate.getTokens()); if (usefulTokens || remainingAmountToCover > 0) { selectedCoveringBoxes.add(boxCandidate); remainingAmountToCover -= boxCandidate.getValue(); tokensRemaining.useTokens(boxCandidate.getTokens()); // if we haven't accounted for a change box so far, but now a change box is // needed, we have to search for a little more amount to cover the change if (!changeBoxConsidered && tokensRemaining.isChangeBoxNeeded()) { changeBoxConsidered = true; remainingAmountToCover = remainingAmountToCover + CHANGE_BOX_NANOERG; amountToSpend = amountToSpend + CHANGE_BOX_NANOERG; } } if (remainingAmountToCover <= 0 && tokensRemaining.areTokensCovered()) return new CoveringBoxes(amountToSpend, selectedCoveringBoxes, tokensToSpend, changeBoxConsidered); else // check the maxBoxToSelect restriction, if it is set if (maxBoxesToSelect > 0 && selectedCoveringBoxes.size() >= maxBoxesToSelect) { List remainingTokenList = tokensRemaining.getRemainingTokenList(); throw new InputBoxesSelectionException.InputBoxLimitExceededException( "Input box limit exceeded, could not cover " + remainingAmountToCover + " nanoERG and " + remainingTokenList.size() + " tokens.", remainingAmountToCover, remainingTokenList, maxBoxesToSelect ); } } } // this chunk is not enough, go to the next (if any) if (chunk.size() == 0) { // this was the last chunk, but still remain to collect assert remainingAmountToCover > 0 || !tokensRemaining.areTokensCovered(); // cannot satisfy the request, but still return cb, with cb.isCovered == false return new CoveringBoxes(amountToSpend, selectedCoveringBoxes, tokensToSpend, changeBoxConsidered); } // step to next chunk page++; } } /** * @return true when boxCandidate is already added to selectedBoxes list */ private static boolean isAlreadyAdded(ArrayList selectedBoxes, InputBox boxCandidate) { boolean alreadyAdded = false; for (InputBox coveringBox : selectedBoxes) { if (coveringBox.getId().equals(boxCandidate.getId())) { alreadyAdded = true; break; } } return alreadyAdded; } /** * Use this interface to adapt behaviour of unspent boxes loading. */ public interface IUnspentBoxesLoader { /** * Called before first call to {@link #loadBoxesPage(BlockchainContext, Address, Integer)} is done * Called before first call to {@link #prepareForAddress(Address)} * * @param ctx {@link BlockchainContext} to work with, if needed * @param addresses addresses boxes will be fetched for * @param grossAmount overall amount of nanoergs needed to satisfy the caller * @param tokensToSpend overall amount of tokens needed to satisfy the caller */ void prepare(@Nonnull BlockchainContext ctx, List

addresses, long grossAmount, @Nonnull List tokensToSpend); /** * Called before first call to {@link #loadBoxesPage(BlockchainContext, Address, Integer)} * for a single address. * * @param address address that will be fetched next */ void prepareForAddress(Address address); /** * @param ctx {@link BlockchainContext} to work with, if needed * @param address p2pk address unspent boxes list should be fetched for * @param page page that should be loaded, 0-based integer * @return a list of InputBox to select from. First items are preferred to be selected. * Returning an empty list means the source of input boxes is drained and no further * page should be loaded */ @Nonnull List loadBoxesPage(@Nonnull BlockchainContext ctx, @Nonnull Address address, @Nonnull Integer page); } /** * Adds a checker method to {@link ExplorerApiUnspentLoader} for boxes that should be omitted * when loading boxes from Explorer API */ public abstract static class ExplorerApiWithCheckerLoader extends ExplorerApiUnspentLoader { protected abstract boolean canUseBox(InputBox box); @Override @Nonnull public List loadBoxesPage(@Nonnull BlockchainContext ctx, @Nonnull Address sender, @Nonnull Integer page) { List boxes = super.loadBoxesPage(ctx, sender, page); List returnedBoxes = new ArrayList<>(boxes.size()); int pageOffset = 0; while (!boxes.isEmpty() && returnedBoxes.isEmpty()) { for (InputBox boxToCheck : boxes) { if (canUseBox(boxToCheck)) returnedBoxes.add(boxToCheck); } // edge case: we had boxes loaded from Explorer, but all were blacklisted... // load next page! // This will make return the same set on next call by getCoveringBoxes, but // getCoveringBoxes handles duplicates so we just have a call too often if (returnedBoxes.isEmpty()) { pageOffset++; boxes = super.loadBoxesPage(ctx, sender, page + pageOffset); } } return returnedBoxes; } } /** * Default loader for unspent boxes. Loads unspent boxes for an address directly from Explorer API */ public static class ExplorerApiUnspentLoader implements IUnspentBoxesLoader { @Override public void prepare(@Nonnull BlockchainContext ctx, List
addresses, long grossAmount, @Nonnull List tokensToSpend) { // not needed } @Override public void prepareForAddress(Address address) { // not needed } @Override @Nonnull public List loadBoxesPage(@Nonnull BlockchainContext ctx, @Nonnull Address address, @Nonnull Integer page) { return ctx.getDataSource().getUnspentBoxesFor(address, page * DEFAULT_LIMIT_FOR_API, DEFAULT_LIMIT_FOR_API); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy