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

io.neow3j.wallet.AssetTransfer Maven / Gradle / Ivy

package io.neow3j.wallet;

import io.neow3j.contract.ScriptHash;
import io.neow3j.crypto.transaction.RawScript;
import io.neow3j.crypto.transaction.RawTransactionAttribute;
import io.neow3j.crypto.transaction.RawTransactionInput;
import io.neow3j.crypto.transaction.RawTransactionOutput;
import io.neow3j.model.types.GASAsset;
import io.neow3j.model.types.TransactionAttributeUsageType;
import io.neow3j.protocol.Neow3j;
import io.neow3j.protocol.core.methods.response.NeoGetContractState;
import io.neow3j.protocol.core.methods.response.NeoSendRawTransaction;
import io.neow3j.protocol.exceptions.ErrorResponseException;
import io.neow3j.transaction.ContractTransaction;
import io.neow3j.utils.Numeric;
import io.neow3j.utils.Strings;
import io.neow3j.wallet.Balances.AssetBalance;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class AssetTransfer {

    private Neow3j neow3j;
    private ContractTransaction tx;
    private Account account;

    private AssetTransfer(Builder builder) {
        this.neow3j = builder.neow3j;
        this.tx = builder.tx;
        this.account = builder.account;
    }

    public ContractTransaction getTransaction() {
        return tx;
    }

    /**
     * 

Adds the given witness to the transaction's witnesses.

*
*

Use this method for adding a custom witness to the transaction. * This does the same as the method {@link Builder#witness(RawScript)}, namely just adds the * provided witness. But here it allows to add a witness from the created transaction object * ({@link AssetTransfer#getTransaction()}) which is not possible in the builder.

* * @param witness The witness to be added. * @return this asset transfer object. */ public AssetTransfer addWitness(RawScript witness) { tx.addScript(witness); return this; } public AssetTransfer send() throws IOException, ErrorResponseException { String rawTx = Numeric.toHexStringNoPrefix(tx.toArray()); NeoSendRawTransaction response = neow3j.sendRawTransaction(rawTx).send(); response.throwOnError(); return this; } /** * Adds a witness to the transaction. The witness is created with the transaction in its current * state and the account involved in this asser transfer. * * @return this asset transfer object, updated with a witness. */ public AssetTransfer sign() { if (account.getPrivateKey() == null) { throw new IllegalStateException("Account does not hold a decrypted private key for " + "signing the transaction. Decrypt the private key before attempting to sign " + "with it."); } tx.addScript(RawScript.createWitness(tx.toArrayWithoutScripts(), account.getECKeyPair())); return this; } public static class Builder { private Neow3j neow3j; private Account account; private BigDecimal networkFee; private List outputs; private List inputs; private Map> utxos; private List witnesses; private List attributes; private InputCalculationStrategy inputCalculationStrategy; private ContractTransaction tx; private String assetId; private String toAddress; private BigDecimal amount; private ScriptHash fromContractScriptHash; public Builder(Neow3j neow3j) { this.neow3j = neow3j; this.outputs = new ArrayList<>(); this.inputs = new ArrayList<>(); this.utxos = new HashMap<>(); this.attributes = new ArrayList<>(); this.witnesses = new ArrayList<>(); this.networkFee = BigDecimal.ZERO; this.inputCalculationStrategy = InputCalculationStrategy.DEFAULT_STRATEGY; } public Builder account(Account account) { this.account = account; return this; } public Builder fromContract(ScriptHash contractScriptHash) { this.fromContractScriptHash = contractScriptHash; return this; } /** * Adds the given transaction output. * * @param output The transaction output to add. * @return this. */ public Builder output(RawTransactionOutput output) { throwIfSingleOutputIsUsed(); this.outputs.add(output); return this; } /** * Adds the given transaction outputs. * * @param outputs The transaction outputs to add. * @return this. */ public Builder outputs(List outputs) { throwIfSingleOutputIsUsed(); this.outputs.addAll(outputs); return this; } /** * Adds the given asset id, amount and receiver address as a transaction output. * * @param assetId The asset id of the output. * @param amount The amount of the output. * @param address The receiving address of the output * @return this. */ public Builder output(String assetId, String amount, String address) { return output(new RawTransactionOutput(assetId, amount, address)); } /** * Adds the given asset id, amount and receiver address as a transaction output. * * @param assetId The asset id of the output. * @param amount The amount of the output. * @param address The receiving address of the output * @return this. */ public Builder output(String assetId, double amount, String address) { return output(new RawTransactionOutput(assetId, amount, address)); } /** * Adds the given unspent transaction outputs (UTXOs) as inputs. They will be used for * covering outputs. * * @param utxos The UTXOs. * @return this. */ public Builder utxos(Utxo... utxos) { Arrays.stream(utxos).forEach(utxo -> { List assetUtxos = this.utxos.computeIfAbsent(utxo.getAssetId(), k -> new ArrayList()); assetUtxos.add(utxo); }); return this; } /** * Adds the given unspent transaction output as an input, which will be used for covering * outputs. * * @param assetId The asset of the unspent output. * @param transactionHash The hash of the transaction in which the unspent output resides. * @param index The index of the unspent output. * @param value The value of the unspent output. * @return this. * @deprecated Use {@link Builder#utxo(String, String, int, double)} or * {@link Builder#utxo(String, String, int, String)} instead. */ @Deprecated public Builder utxo(String assetId, String transactionHash, int index, BigDecimal value) { return utxos(new Utxo(assetId, transactionHash, index, value)); } /** * Adds the given unspent transaction output as an input, which will be used for covering * outputs. * * @param assetId The asset of the unspent output. * @param transactionHash The hash of the transaction in which the unspent output resides. * @param index The index of the unspent output. * @param value The value of the unspent output. * @return this. */ public Builder utxo(String assetId, String transactionHash, int index, double value) { return utxos(new Utxo(assetId, transactionHash, index, value)); } /** * Adds the given unspent transaction output as an input, which will be used for covering * outputs. * * @param assetId The asset of the unspent output. * @param transactionHash The hash of the transaction in which the unspent output resides. * @param index The index of the unspent output. * @param value The value of the unspent output. * @return this. */ public Builder utxo(String assetId, String transactionHash, int index, String value) { return utxos(new Utxo(assetId, transactionHash, index, value)); } public Builder witness(RawScript script) { this.witnesses.add(script); return this; } /** *

Adds the given asset id. This defines which asset should be transferred.

*
*

Use this in combination with {@link Builder#toAddress(String)} and * {@link Builder#amount(double)} to specify a complete transaction output. Alternatively, * you can use {@link Builder#output(String, String, String) output(...)} which allows you * to add all three arguments at the same time.

* * @param assetId The asset id. * @return this. */ public Builder asset(String assetId) { throwIfOutputsAreSet(); this.assetId = assetId; return this; } /** *

Adds the given address which is used as the receiving address in the transfer.

*
*

Use this in combination with {@link Builder#asset(String)} and * {@link Builder#amount(double)} to specify a complete transaction output. Alternatively, * you can use {@link Builder#output(String, String, String) output(...)} which allows you * to add all three arguments at the same time.

* * @param address the receiver's address. * @return this. */ public Builder toAddress(String address) { throwIfOutputsAreSet(); this.toAddress = address; return this; } /** * Specifies the asset amount to spend in the transfer. * * @param amount The amount to transfer. * @return this Builder object. * @deprecated Use {@link Builder#amount(double)} or {@link Builder#amount(String)} * instead. */ @Deprecated public Builder amount(BigDecimal amount) { throwIfOutputsAreSet(); this.amount = amount; return this; } /** *

Specifies the asset amount to spend in the transfer.

*
*

Use this in combination with {@link Builder#asset(String)} and * {@link Builder#toAddress(String)} to specify a complete transaction output. Alternatively, * you can use {@link Builder#output(String, String, String) output(...)} which allows you * to add all three arguments at the same time.

* * @param amount The amount to transfer. * @return this Builder object. */ public Builder amount(String amount) { throwIfOutputsAreSet(); this.amount = new BigDecimal(amount); return this; } /** * Specifies the asset amount to spend in the transfer. * * @param amount The amount to transfer. * @return this Builder object. * @see #amount(String) */ public Builder amount(double amount) { return amount(Double.toString(amount)); } public Builder attribute(TransactionAttributeUsageType type, byte[] value) { return attribute(new RawTransactionAttribute(type, value)); } public Builder attribute(TransactionAttributeUsageType type, String value) { return attribute(new RawTransactionAttribute(type, value)); } public Builder attribute(RawTransactionAttribute attribute) { this.attributes.add(attribute); return this; } public Builder attributes(List attributes) { this.attributes.addAll(attributes); return this; } /** *

Adds a network fee to the transfer.

*
*

Network fees add priority to a transaction and are paid in GAS. If a fee is added the * GAS will be taken from the account used in the asset transfer.

* * @param networkFee The fee amount to add. * @return this Builder object. * @deprecated Use {@link Builder#amount(String)} or {@link Builder#amount(double)} instead. */ @Deprecated public Builder networkFee(BigDecimal networkFee) { this.networkFee = networkFee; return this; } /** *

Adds a network fee to the transfer.

*
*

Network fees add priority to a transaction and are paid in GAS. If a fee is added the * GAS will be taken from the account used in the asset transfer.

* * @param networkFee The fee amount to add. * @return this Builder object. */ public Builder networkFee(String networkFee) { this.networkFee = new BigDecimal(networkFee); return this; } /** * Adds a network fee to the transfer. * * @param networkFee The fee amount to add. * @return this Builder object. * @see Builder#networkFee(String) */ public Builder networkFee(double networkFee) { return networkFee(Double.toString(networkFee)); } /** * Add the strategy that will be used to calculate the UTXOs used as transaction inputs. * * @param strategy The strategy to use. * @return this Builder object. */ public Builder inputCalculationStrategy(InputCalculationStrategy strategy) { this.inputCalculationStrategy = strategy; return this; } public AssetTransfer build() { if (neow3j == null) throw new IllegalStateException("Neow3j not set"); if (account == null) throw new IllegalStateException("Account not set"); if (outputs.isEmpty()) { if (allSingleOutputAttributesSet()) { outputs.add(new RawTransactionOutput(assetId, amount.toPlainString(), toAddress)); } else { throw new IllegalStateException("No or incomplete transaction outputs set"); } } List intents = new ArrayList<>(); intents.addAll(outputs); intents.addAll(createOutputsFromFees(networkFee)); Map requiredAssets = calculateRequiredAssetsForIntents(intents); if (fromContractScriptHash == null) { handleNormalTransfer(requiredAssets); } else { handleTransferFromContract(requiredAssets); } this.tx = buildTransaction(); return new AssetTransfer(this); } private void handleNormalTransfer(Map requiredAssets) { if (this.utxos.isEmpty()) { fetchUtxosFromAccount(this.account, requiredAssets.keySet()); } calculateInputsAndChange(requiredAssets, this.account); } private void fetchUtxosFromAccount(Account acct, Set requiredAssets) { requiredAssets.forEach(assetId -> { AssetBalance balance = acct.getAssetBalance(assetId); List assetUtxos = this.utxos.computeIfAbsent(assetId, k -> new ArrayList<>()); assetUtxos.addAll(balance.getUtxos()); }); } private void handleTransferFromContract(Map requiredAssets) { Account contractAcct = Account.fromAddress(fromContractScriptHash.toAddress()).build(); if (this.utxos.isEmpty()) { try { contractAcct.updateAssetBalances(neow3j); } catch (Exception e) { throw new RuntimeException("Failed to fetch UTXOs for the contract with " + "script hash " + fromContractScriptHash.toString(), e); } fetchUtxosFromAccount(contractAcct, requiredAssets.keySet()); } calculateInputsAndChange(requiredAssets, contractAcct); // Because in a transaction that withdraws from a contract address the transaction // inputs are coming from the contract, there are now inputs from the account that // initiates the transfer. Therefore it needs to be mentioned in an script attribute. attributes.add(new RawTransactionAttribute( TransactionAttributeUsageType.SCRIPT, account.getScriptHash().toArray())); NeoGetContractState contractState; try { contractState = neow3j.getContractState(fromContractScriptHash.toString()).send(); contractState.throwOnError(); } catch (Exception e) { throw new RuntimeException("Failed to fetch contract information for the " + "contract with script hash " + fromContractScriptHash.toString(), e); } int nrOfParams = contractState.getContractState().getContractParameters().size(); byte[] invocationScript = Numeric.hexStringToByteArray(Strings.zeros(nrOfParams * 2)); witnesses.add(new RawScript(invocationScript, fromContractScriptHash)); } private void calculateInputsAndChange(Map requiredAssets, Account changeAcct) { requiredAssets.forEach((assetId, requiredAmount) -> { List selectedUtxos = this.inputCalculationStrategy.calculateInputs( this.utxos.get(assetId), requiredAmount); this.inputs.addAll(selectedUtxos.stream() .map(Utxo::toTransactionInput) .collect(Collectors.toList())); RawTransactionOutput change = getChangeTransactionOutput(assetId, requiredAmount, selectedUtxos, changeAcct.getAddress()); if (change != null) { this.outputs.add(change); } }); } private RawTransactionOutput getChangeTransactionOutput(String assetId, BigDecimal requiredValue, List utxos, String changeAddress) { BigDecimal inputAmount = utxos.stream().map(Utxo::getValue).reduce(BigDecimal::add).get(); if (inputAmount.compareTo(requiredValue) <= 0) { return null; } BigDecimal change = inputAmount.subtract(requiredValue); return new RawTransactionOutput(assetId, change.toPlainString(), changeAddress); } private ContractTransaction buildTransaction() { return new ContractTransaction.Builder() .outputs(this.outputs) .inputs(this.inputs) .scripts(this.witnesses) .attributes(this.attributes) .build(); } private List createOutputsFromFees(BigDecimal... fees) { List outputs = new ArrayList<>(fees.length); for (BigDecimal fee : fees) { if (fee.compareTo(BigDecimal.ZERO) > 0) { outputs.add(new RawTransactionOutput(GASAsset.HASH_ID, fee.toPlainString(), null)); } } return outputs; } private Map calculateRequiredAssetsForIntents( List outputs) { Map assets = new HashMap<>(); outputs.forEach(output -> { BigDecimal value = new BigDecimal(output.getValue()); if (assets.containsKey(output.getAssetId())) { value = assets.get(output.getAssetId()).add(value); } assets.put(output.getAssetId(), value); }); return assets; } private void throwIfOutputsAreSet() { if (!outputs.isEmpty()) { throw new IllegalStateException("Don't set transaction outputs and use the " + "single output methods `asset()`, `toAddress()` and `amount()` " + "simultaneously"); } } private void throwIfSingleOutputIsUsed() { if (amount != null || toAddress != null || assetId != null) { throw new IllegalStateException("Don't set transaction outputs and use the " + "single output methods `asset()`, `toAddress()` and `amount()` " + "simultaneously"); } } private boolean allSingleOutputAttributesSet() { return amount != null && toAddress != null && assetId != null; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy