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

io.neow3j.contract.NFToken Maven / Gradle / Ivy

package io.neow3j.contract;

import io.neow3j.contract.exceptions.UnexpectedReturnTypeException;
import io.neow3j.model.types.StackItemType;
import io.neow3j.protocol.Neow3j;
import io.neow3j.protocol.core.methods.response.NFTokenProperties;
import io.neow3j.protocol.core.methods.response.StackItem;
import io.neow3j.transaction.Signer;
import io.neow3j.utils.Numeric;
import io.neow3j.wallet.Wallet;
import io.neow3j.wallet.exceptions.InsufficientFundsException;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Represents a NEP-11 Non-Fungible Token contract and provides methods to invoke it.
 */
public class NFToken extends Token {

    private static final String TRANSFER = "transfer";
    private static final String OWNER_OF = "ownerOf";
    private static final String BALANCE_OF = "balanceOf";
    private static final String TOKENS_OF = "tokensOf";
    private static final String PROPERTIES = "properties";

    /**
     * Constructs a new {@code NFT} representing the contract with the given script
     * hash. Uses the given {@link Neow3j} instance for all invocations.
     *
     * @param scriptHash the token contract's script hash.
     * @param neow the {@link Neow3j} instance to use for invocations.
     */
    public NFToken(ScriptHash scriptHash, Neow3j neow) {
        super(scriptHash, neow);
    }

    /**
     * Transfers the token with {@code tokenID} to the account {@code to}.
     *
     * @param wallet the wallet that holds the account of the token owner.
     * @param to the receiver of the token.
     * @param tokenID the token ID.
     * @return a transaction builder.
     * @throws IOException if there was a problem fetching information from the Neo node.
     */
    public TransactionBuilder transfer(Wallet wallet, ScriptHash to, byte[] tokenID) throws IOException {
        int decimals = getDecimals();
        if (decimals != 0) {
            throw new IllegalStateException("This method is only implemented on NF tokens that are " +
                    "indivisible. This token has " + decimals + " decimals.");
        }

        List owners = ownerOf(tokenID);
        if (owners.size() != 1) {
            throw new IllegalStateException("The token with ID " + Numeric.toHexString(tokenID) + " has " +
                    owners.size() + " owners. To transfer fractions use the method transferFractions.");
        }

        ScriptHash tokenOwner = owners.get(0);
        if (!wallet.holdsAccount(tokenOwner)) {
            throw new IllegalArgumentException("The provided wallet does not contain the account that " +
                    "owns the token with ID " + Numeric.toHexString(tokenID) + ". The address of the " +
                    "owner of this token is " + tokenOwner.toAddress() + ".");
        }

        return invokeFunction(TRANSFER,
                ContractParameter.hash160(to),
                ContractParameter.byteArray(tokenID))
                .wallet(wallet)
                .signers(Signer.calledByEntry(tokenOwner));
    }

    /**
     * Transfers the {@code amount} of the token with {@code tokenID} to the account {@code to}.
     * The {@code wallet} has to contain the {@code from} account and that account has to be in possession of
     * {@code amount} fractions of the token with the given {@code tokenID}.
     *
     * @param wallet the wallet that holds the {@code from} account.
     * @param from the account to send the token fractions from.
     * @param to the receiver of the token fraction.
     * @param amount the amount of the token to transfer as a decimal number (not token fractions).
     * @param tokenID the token ID.
     * @return a transaction builder.
     * @throws IOException if there was a problem fetching information from the Neo node.
     */
    public TransactionBuilder transferFraction(Wallet wallet, ScriptHash from, ScriptHash to,
            BigDecimal amount, byte[] tokenID) throws IOException {
        List owners = ownerOf(tokenID);
        if (amount.compareTo(BigDecimal.ZERO) < 0 || amount.compareTo(BigDecimal.ONE) > 0) {
            throw new IllegalArgumentException("The amount to transfer must be in the range of 0 to 1.");
        }

        if (amount.scale() > getDecimals()) {
            throw new IllegalArgumentException("The scale of the provided amount is higher than the " +
                    "decimals of this token contract.");
        }

        if (owners.stream().noneMatch(from::equals)) {
            throw new IllegalStateException("The account " + from.toAddress() + " is not an owner " +
                    "of the token with ID " + Numeric.toHexString(tokenID) + ".");
        }

        BigInteger balanceInFractions = balanceOf(from, tokenID);
        BigDecimal balance = getAmountAsBigDecimal(balanceInFractions);
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientFundsException("The provided account can not cover the given amount. " +
                    "The amount to transfer was " + amount + " but only " + balance + " is held by the " +
                    "given account.");
        }

        if (!wallet.holdsAccount(from)) {
            throw new IllegalArgumentException("The provided wallet does not contain the provided from " +
                    "account.");
        }

        BigInteger fractions = getAmountAsBigInteger(amount);

        return invokeFunction(TRANSFER,
                ContractParameter.hash160(from),
                ContractParameter.hash160(to),
                ContractParameter.integer(fractions),
                ContractParameter.byteArray(tokenID))
                .wallet(wallet)
                .signers(Signer.calledByEntry(from));
    }

    /**
     * Gets the owner(s) of the token with {@code tokenID}.
     *
     * @param tokenID the token ID.
     * @return a list of owners of the token.
     * @throws IOException if an error occurs when interacting with the neo-node
     */
    // According to the not yet final NEP-11 proposal, this method returns an enumerator that contains all
    //  the co-owners that own the specified token. Hence, this method expects an array stack item
    //  containing the co-owners.
    // TODO: 15.10.20 Michael: Adapt this method as soon as an implementation of the NEP-11 proposal exists.
    public List ownerOf(byte[] tokenID) throws IOException {
        return callFunctionReturningListOfScriptHashes(OWNER_OF,
                Arrays.asList(ContractParameter.byteArray(tokenID)));
    }

    private List callFunctionReturningListOfScriptHashes(String function,
            List params) throws IOException {

        StackItem arrayItem = callInvokeFunction(function, params).getInvocationResult().getStack().get(0);
        if (!arrayItem.getType().equals(StackItemType.ARRAY)) {
            throw new UnexpectedReturnTypeException(arrayItem.getType(), StackItemType.ARRAY);
        }
        List scriptHashes = new ArrayList<>();
        for (StackItem item : arrayItem.asArray().getValue()) {
            scriptHashes.add(extractScriptHash(item));
        }
        return scriptHashes;
    }

    private ScriptHash extractScriptHash(StackItem item) {
        if (!item.getType().equals(StackItemType.BYTE_STRING)) {
            throw new UnexpectedReturnTypeException(item.getType(),
                    StackItemType.BYTE_STRING);
        }
        try {
            return ScriptHash.fromAddress(item.asByteString().getAsAddress());
        } catch (IllegalArgumentException e) {
            throw new UnexpectedReturnTypeException("Byte array return type did not contain "
                    + "script hash in expected format.", e);
        }
    }

    /**
     * Gets the balance of the token with {@code tokenID} for the given account.
     * 

* The balance is returned in token fractions. E.g., a balance of 0.5 of a token with 2 decimals * is returned as 50 (= 0.5 * 10^2) token fractions. *

* The balance is not cached locally. Every time this method is called requests are send to the * neo-node. * * @param owner the script hash of the account to fetch the balance for. * @param tokenID the token ID. * @return the token balance of the given account. * @throws IOException if there was a problem fetching information from the * Neo node. * @throws UnexpectedReturnTypeException if the contract invocation did not return something * interpretable as a number. */ public BigInteger balanceOf(ScriptHash owner, byte[] tokenID) throws IOException { return callFuncReturningInt(BALANCE_OF, ContractParameter.hash160(owner), ContractParameter.byteArray(tokenID)); } /** * Gets all the token IDs owned by the given account. * * @param owner the account. * @return a list of all token IDs that are owned by the given account. * @throws IOException if an error occurs when interacting with the neo-node */ public List tokensOf(ScriptHash owner) throws IOException { return callFunctionReturningListOfScriptHashes(TOKENS_OF, Arrays.asList(ContractParameter.hash160(owner))); } /** * Gets the properties of the token with {@code tokenID}. * * @param tokenID the token ID. * @return the properties of the token. * @throws IOException if an error occurs when interacting with the neo-node */ public NFTokenProperties properties(byte[] tokenID) throws IOException { StackItem item = callInvokeFunction(PROPERTIES, Arrays.asList(ContractParameter.byteArray(tokenID))) .getInvocationResult().getStack().get(0); if (item.getType().equals(StackItemType.BYTE_STRING)) { return item.asByteString().getAsJson(NFTokenProperties.class); } throw new UnexpectedReturnTypeException(item.getType(), StackItemType.BYTE_STRING); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy