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

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

package io.neow3j.wallet;

import io.neow3j.types.Hash160;
import io.neow3j.crypto.ECKeyPair;
import static io.neow3j.crypto.SecurityProviderChecker.addBouncyCastle;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.neow3j.crypto.NEP2;
import io.neow3j.crypto.ScryptParams;
import io.neow3j.crypto.exceptions.CipherException;
import io.neow3j.crypto.exceptions.NEP2InvalidFormat;
import io.neow3j.crypto.exceptions.NEP2InvalidPassphrase;
import io.neow3j.protocol.Neow3j;
import io.neow3j.script.VerificationScript;
import io.neow3j.wallet.nep6.NEP6Account;
import io.neow3j.wallet.nep6.NEP6Wallet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URI;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * The wallet manages a collection of accounts. Exactly one of these contained accounts is
 * the default account of this wallet, which is used, e.g., when doing contract invocations
 * and no account is mentioned specifically.
 */
public class Wallet {

    private static final String DEFAULT_WALLET_NAME = "neow3jWallet";
    public static final String CURRENT_VERSION = "3.0";
    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private String name;
    private String version;
    private Map accounts = new HashMap<>();
    private ScryptParams scryptParams;
    private Hash160 defaultAccount;

    static {
        addBouncyCastle();
    }

    private Wallet() {
        this.name = DEFAULT_WALLET_NAME;
        this.version = CURRENT_VERSION;
        this.scryptParams = NEP2.DEFAULT_SCRYPT_PARAMS;
    }

    public String getName() {
        return name;
    }

    public String getVersion() {
        return version;
    }

    public List getAccounts() {
        return accounts.entrySet().stream()
                .sorted(Entry.comparingByKey())
                .map(Entry::getValue)
                .collect(Collectors.toList());
    }

    /**
     * Sets the account with the given script hash to the default account of this wallet.
     *
     * @param accountHash160 The new default account.
     * @throws IllegalArgumentException if the given account is not in this wallet.
     * @return the Wallet
     */
    public Wallet defaultAccount(Hash160 accountHash160) {
        if (accountHash160 == null) throw new IllegalArgumentException("No account provided to set default.");
        if (!this.accounts.containsKey(accountHash160)) {
            throw new IllegalArgumentException("Cannot set default account on wallet. Wallet does "
                    + "not contain the account with script hash "
                    + accountHash160.toString() + ".");
        }
        this.defaultAccount = accountHash160;
        return this;
    }

    public ScryptParams getScryptParams() {
        return scryptParams;
    }

    /**
     * Gets the default account of this wallet.
     *
     * @return the default account.
     */
    public Account getDefaultAccount() {
        return this.accounts.get(this.defaultAccount);
    }

    /**
     * Checks whether an account is the default account in the wallet.
     *
     * @param account the account to be checked.
     * @return Whether the given account is the default account in this wallet.
     */
    public Boolean isDefault(Account account) {
        return isDefault(account.getScriptHash());
    }

    /**
     * Checks whether an account is the default account in the wallet.
     *
     * @param accountHash160 the account to be checked.
     * @return Whether the given account is the default account in this wallet.
     */
    public Boolean isDefault(Hash160 accountHash160) {
        return getDefaultAccount().getScriptHash().equals(accountHash160);
    }

    public Wallet name(String name) {
        this.name = name;
        return this;
    }

    public Wallet version(String version) {
        this.version = version;
        return this;
    }

    public Wallet scryptParams(ScryptParams scryptParams) {
        this.scryptParams = scryptParams;
        return this;
    }

    /**
     * Adds the given accounts to this wallet, if it doesn't contain an account with the same
     * script hash (address).
     *
     * @param accounts The accounts to add.
     * @return This wallet instance.
     */
    public Wallet addAccounts(Account... accounts) {
        for (Account acct : accounts) {
            if (this.accounts.containsKey(acct.getScriptHash())) {
                continue;
            }
            // An account is only allowed to be in one wallet at a time.
            if (acct.getWallet() != null) throw new IllegalArgumentException("The account " + acct.getAddress() +
                    " is already contained in a wallet. Please remove this account from its containing wallet" +
                    " before adding it to another wallet.");
            this.accounts.put(acct.getScriptHash(), acct);
            // Create a link for the account
            acct.setWallet(this);
        }
        return this;
    }

    /**
     * Removes the account from this wallet.
     * If there is only one account in the wallet left, this account can not be removed.
     *
     * @param account the account to be removed.
     * @return true if an account was removed, false if no account with the given address was found.
     */
    public boolean removeAccount(Account account) {
        return removeAccount(account.getScriptHash());
    }

    /**
     * Removes the account with the given script hash (address) from this wallet.
     * If there is only one account in the wallet left, this account can not be removed.
     *
     * @param hash160 The {@link Hash160} of the account to be removed.
     * @return true if an account was removed, false if no account with the given address was found.
     */
    public boolean removeAccount(Hash160 hash160) {
        if (!this.accounts.containsKey(hash160)) {
            return false;
        }
        // The wallet must have at least one account at all times.
        if (this.accounts.size() == 1) {
            throw new IllegalArgumentException("The account " + hash160.toAddress() +
                    " is the only account in the wallet. It cannot be removed.");
        }
        // Remove the link to this wallet in the account instance.
        this.accounts.get(hash160).setWallet(null);

        // If the removed account was the default account in this wallet, set a new default account.
        if (hash160.equals(this.getDefaultAccount().getScriptHash())) {
            Hash160 newDefaultAccountHash160 = this.accounts.entrySet().stream()
                    .filter(e -> !e.getKey().equals(hash160))
                    .iterator().next().getKey();
            this.defaultAccount(newDefaultAccountHash160);
        }
        return accounts.remove(hash160) != null;
    }

    public void decryptAllAccounts(String password)
            throws NEP2InvalidFormat, CipherException, NEP2InvalidPassphrase {

        for (Entry e : accounts.entrySet()) {
            e.getValue().decryptPrivateKey(password, scryptParams);
        }
    }

    public void encryptAllAccounts(String password) throws CipherException {
        for (Entry e : accounts.entrySet()) {
            e.getValue().encryptPrivateKey(password, scryptParams);
        }
    }

    public NEP6Wallet toNEP6Wallet() {
        List accts = this.accounts.values().stream()
                .map(Account::toNEP6Account)
                .collect(Collectors.toList());
        return new NEP6Wallet(name, version, scryptParams, accts, null);
    }

    public static Wallet fromNEP6Wallet(String nep6WalletFileName) throws IOException {
        return fromNEP6Wallet(
                Wallet.class.getClassLoader().getResourceAsStream(nep6WalletFileName));
    }

    public static Wallet fromNEP6Wallet(URI nep6WalletFileUri) throws IOException {
        return fromNEP6Wallet(nep6WalletFileUri.toURL().openStream());
    }

    public static Wallet fromNEP6Wallet(File nep6WalletFile) throws IOException {
        return fromNEP6Wallet(new FileInputStream(nep6WalletFile));
    }

    public static Wallet fromNEP6Wallet(InputStream nep6WalletFileInputStream) throws IOException {
        NEP6Wallet nep6Wallet = OBJECT_MAPPER
                .readValue(nep6WalletFileInputStream, NEP6Wallet.class);
        return fromNEP6Wallet(nep6Wallet);
    }

    public static Wallet fromNEP6Wallet(NEP6Wallet nep6Wallet) {
        Account[] accs = nep6Wallet.getAccounts().stream()
                .map(Account::fromNEP6Account)
                .toArray(Account[]::new);

        Optional defaultAccount = nep6Wallet.getAccounts().stream()
                .filter(NEP6Account::getDefault)
                .findFirst();

        if (defaultAccount.isPresent()) {
            Hash160 defaultAccountHash160 = Account.fromNEP6Account(defaultAccount.get()).getScriptHash();
            return new Wallet()
                    .name(nep6Wallet.getName())
                    .version(nep6Wallet.getVersion())
                    .scryptParams(nep6Wallet.getScrypt())
                    .addAccounts(accs)
                    .defaultAccount(defaultAccountHash160);
        } else {
            throw new IllegalArgumentException("The Nep-6 wallet does not contain any default account.");
        }
    }

    /**
     * Creates a NEP6 compatible wallet file.
     *
     * @param destination the file that the wallet file should be saved.
     * @return the new wallet.
     * @throws IOException throws if failed to create the wallet on disk.
     */
    public Wallet saveNEP6Wallet(File destination) throws IOException {
        if (destination == null) {
            throw new IllegalArgumentException("Destination file cannot be null");
        }
        NEP6Wallet nep6Wallet = toNEP6Wallet();
        if (destination.isDirectory()) {
            String fileName = getName() + ".json";
            destination = Paths.get(destination.toString(), fileName).toFile();
        }
        OBJECT_MAPPER.writeValue(destination, nep6Wallet);
        return this;
    }

    /**
     * Gets the balances of all NEP-17 tokens that this wallet owns.
     * 

* The token amounts are returned in token fractions. E.g., an amount of 1 GAS is returned as * 1*10^8 GAS fractions. *

* Requires on a neo-node with the RpcNep17Tracker plugin installed. The balances are not cached * locally. Every time this method is called requests are send to the neo-node for all contained * accounts. * * @param neow3j The {@link Neow3j} object used to call a neo-node. * @return the map of token script hashes to token amounts. * @throws IOException If something goes wrong when communicating with the neo-node. */ public Map getNep17TokenBalances(Neow3j neow3j) throws IOException { Map balances = new HashMap<>(); for (Account a : this.accounts.values()) { for (Entry e : a.getNep17Balances(neow3j).entrySet()) { balances.merge(e.getKey(), e.getValue(), BigInteger::add); } } return balances; } /** * Creates a new wallet with one account. * * @return the new wallet. */ public static Wallet create() { Account a = Account.create(); return new Wallet().addAccounts(a).defaultAccount(a.getScriptHash()); } /** * Creates a new wallet with one account that is set as the default account. Encrypts such * account with the password. * * @param password password used to encrypt the account. * @return the new wallet. * @throws CipherException throws if failed encrypt the created wallet. */ public static Wallet create(final String password) throws CipherException { Wallet w = create(); w.encryptAllAccounts(password); return w; } /** * Creates a new wallet with one account that is set as the default account. Also, encrypts such * account and persists the NEP6 wallet to a file. * * @param password password used to encrypt the account. * @param destination destination to the new NEP6 wallet file. * @return the new wallet. * @throws IOException throws if failed to create the wallet on disk. * @throws CipherException throws if failed encrypt the created wallet. */ public static Wallet create(String password, File destination) throws CipherException, IOException { Wallet wallet = create(password); wallet.saveNEP6Wallet(destination); return wallet; } /** * Creates a new wallet with the given accounts. * The first account is set as the default account. * * @param accounts the accounts to add to the new wallet. * @return the new wallet. */ public static Wallet withAccounts(Account... accounts) { if (accounts.length == 0) { throw new IllegalArgumentException("No accounts provided to initialize a wallet."); } return new Wallet() .addAccounts(accounts) .defaultAccount(accounts[0].getScriptHash()); } public boolean holdsAccount(Hash160 hash160) { return this.accounts.containsKey(hash160); } /** * Gets the account with the given script hash if it is in this wallet. * @param hash160 The script hash of the account. * @return the account if it is in this wallet. Null, otherwise. */ public Account getAccount(Hash160 hash160) { return this.accounts.get(hash160); } /** * Checks whether the wallet holds all the required private keys for a multi-sig account. * * @param multiSigVerificationScript the verification script of the multi-sig account. * @return whether the wallet holds all the required private keys for the multi-sig account. */ public boolean privateKeysArePresentForMultiSig(VerificationScript multiSigVerificationScript) { int signers = 0; Account account; for (ECKeyPair.ECPublicKey pubKey : multiSigVerificationScript.getPublicKeys()) { Hash160 hash160 = Hash160.fromPublicKey(pubKey.getEncoded(true)); if (holdsAccount(hash160)) { account = getAccount(hash160); if (account != null && account.getECKeyPair() != null) { signers += 1; } } } int signingThreshold = multiSigVerificationScript.getSigningThreshold(); return signers >= signingThreshold; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy