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

convex.api.Convex Maven / Gradle / Ivy

There is a newer version: 0.7.15
Show newest version
package convex.api;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import convex.core.Constants;
import convex.core.ErrorCodes;
import convex.core.Result;
import convex.core.crypto.AKeyPair;
import convex.core.data.ACell;
import convex.core.data.AList;
import convex.core.data.AString;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.Address;
import convex.core.data.Hash;
import convex.core.data.Keywords;
import convex.core.data.Lists;
import convex.core.data.Ref;
import convex.core.data.SignedData;
import convex.core.data.prim.CVMLong;
import convex.core.exceptions.MissingDataException;
import convex.core.lang.RT;
import convex.core.lang.Reader;
import convex.core.lang.Symbols;
import convex.core.lang.ops.Lookup;
import convex.core.lang.ops.Special;
import convex.core.store.AStore;
import convex.core.store.Stores;
import convex.core.transactions.ATransaction;
import convex.core.transactions.Invoke;
import convex.core.transactions.Transfer;
import convex.core.util.Utils;
import convex.core.State;
import convex.net.ResultConsumer;
import convex.net.message.Message;
import convex.peer.Server;

/**
 * Class representing a client API to the Convex network.
 *
 * An Object of the type Convex represents a stateful client connection to the
 * Convex network that can issue transactions both synchronously and
 * asynchronously. This can be used by both peers and JVM-based clients.
 *
 * "I'm doing a (free) operating system (just a hobby, won't be big and
 * professional like gnu)" - Linus Torvalds
 */
@SuppressWarnings("unused")
public abstract class Convex {

	private static final Logger log = LoggerFactory.getLogger(Convex.class.getName());

	protected long timeout = Constants.DEFAULT_CLIENT_TIMEOUT;

	/**
	 * Key pair for this Client
	 */
	protected AKeyPair keyPair;

	/**
	 * Current Address for this Client
	 */
	protected Address address;

	/**
	 * Determines if auto-sequencing should be attempted. Default to true.
	 */
	private boolean autoSequence = true;

	/**
	 * Sequence number for this client, or null if not yet known. Used to number new
	 * transactions if not otherwise specified.
	 */
	protected Long sequence = null;

	/**
	 * Map of results awaiting completion. May be pending missing data.
	 */
	protected HashMap> awaiting = new HashMap<>();

	protected final Consumer internalHandler = new ResultConsumer() {
		@Override
		protected synchronized void handleResult(long id, Result v) {

			if ((v != null) && (ErrorCodes.SEQUENCE.equals(v.getErrorCode()))) {
				// We probably got a wrong sequence number. Kill the stored value.
				sequence = null;
			}

			// TODO: maybe extract method?
			synchronized (awaiting) {
				CompletableFuture cf = awaiting.get(id);
				if (cf != null) {
					awaiting.remove(id);
					cf.complete(v);
					log.debug("Completed Result received for message ID: {}", id);
				} else {
					log.debug("Ignored Result received for unexpected message ID: {}", id);
				}
			}
		}

		@Override
		public void accept(Message m) {
			super.accept(m);

			if (delegatedHandler != null) {
				try {
					delegatedHandler.accept(m);
				} catch (Throwable t) {
					log.warn("Exception thrown in user-supplied handler function: {}", t);
				}
			}
		}
	};

	private Consumer delegatedHandler = null;

	protected Convex(Address address, AKeyPair keyPair) {
		this.keyPair = keyPair;
		this.address = address;
	}

	/**
	 * Creates an anonymous connection to a Peer, suitable for queries
	 * 
	 * @param hostAddress Address of Peer
	 * @return New Convex client instance
	 * @throws IOException      If IO Error occurs
	 * @throws TimeoutException If connection attempt times out
	 */
	public static ConvexRemote connect(InetSocketAddress hostAddress) throws IOException, TimeoutException {
		return connect(hostAddress, (Address) null, (AKeyPair) null);
	}

	/**
	 * Create a Convex client by connecting to the specified Peer using the given
	 * key pair
	 *
	 * @param peerAddress Address of Peer
	 * @param address     Address of Account to use for Client
	 * @param keyPair     Key pair to use for client transactions
	 * @return New Convex client instance
	 * @throws IOException      If connection fails due to IO error
	 * @throws TimeoutException If connection attempt times out
	 */
	public static ConvexRemote connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair)
			throws IOException, TimeoutException {
		return Convex.connect(peerAddress, address, keyPair, Stores.current());
	}

	/**
	 * Create a remote connection to a Convex Server in the same JVM.
	 * 
	 * @param server Server instance to connect to.
	 * @return Convex client instance
	 * @throws IOException      If connection fails due to IO error
	 * @throws TimeoutException If connection attempt times out
	 */
	public static ConvexRemote connectRemote(Server server) throws IOException, TimeoutException {
		return connect(server.getHostAddress(), server.getPeerController(), server.getKeyPair());
	}

	/**
	 * Create a Convex client by connecting to the specified Peer using the given
	 * key pair and using a given store
	 *
	 * @param peerAddress Address of Peer
	 * @param address     Address of Account to use for Client
	 * @param keyPair     Key pair to use for client transactions
	 * @param store       Store to use for this connection
	 * @return New Convex client instance
	 * @throws IOException      If connection fails due to IO error
	 * @throws TimeoutException If connection attempt times out
	 */
	public static ConvexRemote connect(InetSocketAddress peerAddress, Address address, AKeyPair keyPair, AStore store)
			throws IOException, TimeoutException {
		ConvexRemote convex = new ConvexRemote(address, keyPair);
		convex.connectToPeer(peerAddress, store);
		return convex;
	}

	/**
	 * Sets the Address for this connection. This will be used for subsequent
	 * transactions and queries. User should also set a new keypair if a different
	 * keypair is required for the new Address.
	 *
	 * @param address Address to use
	 */
	public synchronized void setAddress(Address address) {
		if (this.address == address)
			return;
		this.address = address;
		// clear sequence, since we don't know the new account sequence number yet
		sequence = null;
	}

	/**
	 * Sets the Address and Keypair for this connection. This will be used for
	 * subsequent transactions and queries.
	 *
	 * @param address Address to use
	 * @param kp      Keypair to use for the given Address
	 */
	public synchronized void setAddress(Address address, AKeyPair kp) {
		setAddress(address);
		setKeyPair(kp);
	}

	public synchronized void setKeyPair(AKeyPair kp) {
		this.keyPair = kp;
	}

	/**
	 * Gets the next sequence number for this Client, which should be used for
	 * building new signed transactions.
	 *
	 * @return Sequence number as a Long value greater than zero
	 */
	private long getIncrementedSequence() {
		long next = getSequence() + 1L;
		sequence = next;
		return next;
	}

	public void setNextSequence(long nextSequence) {
		this.sequence = nextSequence - 1L;
	}

	public void setHandler(Consumer handler) {
		this.delegatedHandler = handler;
	}

	/**
	 * Gets the current sequence number for this Client, which is the sequence
	 * number of the last transaction observed for the current client's Account.
	 * Will attempt to acquire the sequence number from the network if not known.
	 * 
	 * The next valid sequence number will be one higher than the result.
	 *
	 * @return Sequence number as a Long value (zero or positive)
	 */
	public long getSequence() {
		if (sequence == null) {
			try {
				Future f = query(Special.forSymbol(Symbols.STAR_SEQUENCE));
				Result r = f.get();
				if (r.isError())
					throw new Error("Error querying *sequence*: " + r.getErrorCode() + " " + r.getValue());
				ACell result = r.getValue();
				if (!(result instanceof CVMLong))
					throw new Error("*sequence* query did not return Long, got: " + result);
				sequence = RT.jvm(result);
			} catch (IOException | InterruptedException | ExecutionException e) {
				throw new Error("Error trying to get sequence number", e);
			}
		}
		return sequence;
	}
	
	/**
	 * Gets the current sequence number for an account, which is the sequence
	 * number of the last transaction observed for the Account.
	 * Will attempt to acquire the sequence number from the network if not known.
	 * 
	 * @param addr Address for which to query the sequence number
	 *
	 * @return Sequence number as a Long value (zero or positive)
	 * @throws IOException If an IO error occurs
	 * @throws TimeoutException If the request times out
	 */
	public long getSequence(Address addr) throws TimeoutException, IOException {
		if (Utils.equals(getAddress(), addr)) return getSequence();
		ACell code= Lists.of(Keywords.SEQUENCE, Lists.of(Symbols.ACCOUNT, addr));
		Result r= querySync(code);
		if (r.isError()) throw new RuntimeException("Error trying to get sequence number: "+r);
		ACell rv=r.getValue();
		if (!(rv instanceof CVMLong)) throw new RuntimeException("Unexpected sequence result type: "+Utils.getClassName(rv));
		long seq=((CVMLong)rv).longValue();
		return seq;
	}
	
	/**
	 * Called after a transaction is submitted to update sequence (if possible)
	 * @param value
	 */
	protected void maybeUpdateSequence(SignedData signed) {
		try {
			ATransaction trans=signed.getValue();
			if (!isAutoSequence()) return;
			if (!Utils.equals(trans.getOrigin(),address)) return;
			Long seq=this.sequence;
			if (seq==null) return;
			seq++;
			if (seq==trans.getSequence()) sequence=seq;
		} catch (Exception e) {
			// do nothing. Shouldn't happen except in some adversarial test cases.
		}
	}

	/**
	 * Signs a value on behalf of this client, using the currently assigned keypair.
	 *
	 * @param    Type of value to sign
	 * @param value Value to sign
	 * @return SignedData instance
	 */
	public  SignedData signData(T value) {
		return keyPair.signData(value);
	}

	/**
	 * Creates a new account with the given public key
	 *
	 * @param publicKey Public key to set for the new account
	 * @return Address of account created
	 * @throws TimeoutException If attempt times out
	 * @throws IOException      If IO error occurs
	 */
	public Address createAccountSync(AccountKey publicKey) throws TimeoutException, IOException {
		Invoke trans = Invoke.create(address, 0, Lists.of(Symbols.CREATE_ACCOUNT, publicKey));
		Result r = transactSync(trans);
		if (r.isError())
			throw new Error("Error creating account: " + r.getErrorCode() + " " + r.getValue());
		return (Address) r.getValue();
	}

	/**
	 * Creates a new account with the given public key
	 *
	 * @param publicKey Public key to set for the new account
	 * @return Address of account created
	 * @throws TimeoutException If attempt times out
	 * @throws IOException      If IO error occurs
	 */
	public CompletableFuture
createAccount(AccountKey publicKey) throws TimeoutException, IOException { Invoke trans = Invoke.create(address, 0, Lists.of(Symbols.CREATE_ACCOUNT, publicKey)); CompletableFuture fr = transact(trans); return fr.thenApply(r -> r.getValue()); } /** * Checks if this Convex client instance has an open connection. * * @return true if connected, false otherwise */ public abstract boolean isConnected(); /** * Updates the given transaction to have the next sequence number. * * @param t Any transaction, for which the correct next sequence number is * desired * @return The updated transaction */ private synchronized ATransaction applyNextSequence(ATransaction t) { if (sequence != null) { // if already we know the next sequence number to be applied, set it return t.withSequence(++sequence); } else { return t.withSequence(getIncrementedSequence()); } } /** * Submits a transaction to the Convex network, returning a future once the * transaction has been successfully queued. Signs the transaction with the * currently set key pair * * Should be thread safe as long as multiple clients do not attempt to submit * transactions for the same account concurrently. * * @param transaction Transaction to execute * @return A Future for the result of the transaction * @throws IOException If the connection is broken, or the send buffer is full */ public synchronized CompletableFuture transact(ATransaction transaction) throws IOException { if (transaction.getOrigin() == null) { transaction = transaction.withOrigin(address); } if (autoSequence && (transaction.getSequence() <= 0)) { // apply sequence if using expected address if (Utils.equals(transaction.getOrigin(), address)) { transaction = applyNextSequence(transaction); } else { // ignore?? } } SignedData signed = keyPair.signData(transaction); CompletableFuture r= transact(signed); return r; } /** * Executes a transaction, compiling the given source code as an Invoke. * * @param code Code to execute * @return A Future for the result of the transaction * @throws IOException If the connection is broken, or the send buffer is full */ public synchronized CompletableFuture transact(String code) throws IOException { ATransaction trans = buildTransaction(code); return transact(trans); } private ATransaction buildTransaction(String code) { ACell form = buildCodeForm(code); return Invoke.create(getAddress(), getIncrementedSequence(), form); } private ACell buildCodeForm(String code) { AList forms = Reader.readAll(code); ACell form; if (forms.count() == 1) { form = forms.get(0); } else { form = forms.cons(Symbols.DO); } return form; } /** * Executes a transaction, compiling the given source code as an Invoke. * * @param code Code to execute * @return A Future for the result of the transaction * @throws IOException If the connection is broken, or the send buffer is * full * @throws TimeoutException If the transaction times out */ public synchronized Result transactSync(String code) throws IOException, TimeoutException { ATransaction trans = buildTransaction(code); return transactSync(trans); } /** * Submits a signed transaction to the Convex network, returning a Future once * the transaction has been successfully queued. * * Updates cached sequence number on best effort basis. * * @param signed Signed transaction to execute * @return A Future for the result of the transaction * @throws IOException If the connection is broken */ public abstract CompletableFuture transact(SignedData signed) throws IOException; /** * Submits a transfer transaction to the Convex network, returning a future once * the transaction has been successfully queued. * * @param target Destination address for transfer * @param amount Amount of Convex Coins to transfer * @return A Future for the result of the transaction * @throws IOException If the connection is broken, or the send buffer is full */ public CompletableFuture transfer(Address target, long amount) throws IOException { ATransaction trans = Transfer.create(getAddress(), 0, target, amount); return transact(trans); } /** * Submits a transfer transaction to the Convex network peer, and waits for * confirmation of the result * * @param target Destination address for transfer * @param amount Amount of Convex Coins to transfer * @return Result of the transaction * @throws IOException If the connection is broken, or the send buffer is * full * @throws TimeoutException If the transaction times out */ public Result transferSync(Address target, long amount) throws IOException, TimeoutException { ATransaction trans = Transfer.create(getAddress(), 0, target, amount); return transactSync(trans); } /** * Submits a transaction synchronously to the Convex network, returning a Result * * @param transaction Transaction to execute * @return The result of the transaction * @throws IOException If the connection is broken * @throws TimeoutException If the attempt to transact with the network is not * confirmed within a reasonable time */ public Result transactSync(SignedData transaction) throws TimeoutException, IOException { return transactSync(transaction, timeout); } /** * Submits a transaction synchronously to the Convex network, returning a Result * * @param transaction Transaction to execute * @return The result of the transaction * @throws IOException If the connection is broken * @throws TimeoutException If the attempt to transact with the network is not * confirmed within a reasonable time */ public Result transactSync(ATransaction transaction) throws TimeoutException, IOException { return transactSync(transaction, timeout); } /** * Submits a signed transaction synchronously to the Convex network, returning a * Result * * @param transaction Transaction to execute * @param timeout Number of milliseconds for timeout * @return The result of the transaction * @throws IOException If the connection is broken * @throws TimeoutException If the attempt to transact with the network is not * confirmed by the specified timeout */ public Result transactSync(ATransaction transaction, long timeout) throws TimeoutException, IOException { // sample time at start of transaction attempt long start = Utils.getTimeMillis(); Result result; Future cf = transact(transaction); // adjust timeout if time elapsed to submit transaction long now = Utils.getTimeMillis(); timeout = Math.max(0L, timeout - (now - start)); try { result = cf.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new Error("Not possible? Since there is no Thread for the future....", e); } return result; } /** * Submits a signed transaction synchronously to the Convex network, returning a * Result * * @param transaction Transaction to execute * @param timeout Number of milliseconds for timeout * @return The result of the transaction * @throws IOException If the connection is broken * @throws TimeoutException If the attempt to transact with the network is not * confirmed by the specified timeout */ public Result transactSync(SignedData transaction, long timeout) throws TimeoutException, IOException { // sample time at start of transaction attempt long start = Utils.getTimeMillis(); Result result; Future cf = transact(transaction); // adjust timeout if time elapsed to submit transaction long now = Utils.getTimeMillis(); timeout = Math.max(0L, timeout - (now - start)); try { result = cf.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new Error("Not possible? Since there is no Thread for the future....", e); } return result; } /** * Submits a query to the Convex network, returning a Future once the query has * been successfully queued. * * @param query Query to execute, as a Form or Op * @return A Future for the result of the query * @throws IOException If the connection is broken, or the send buffer is full */ public CompletableFuture query(ACell query) throws IOException { return query(query, getAddress()); } /** * Submits a query to the Convex network, returning a Future once the query has * been successfully queued. * * @param query Query to execute, as String containing one or more forms * @return A Future for the result of the query * @throws IOException If the connection is broken, or the send buffer is full */ public CompletableFuture query(String query) throws IOException { ACell form = buildCodeForm(query); return query(form, getAddress()); } /** * Attempts to acquire a complete persistent data structure for the given hash * from the remote peer. Uses the current store configured for the calling * thread. * * @param hash Hash of value to acquire. * * @return Future for the cell being acquired */ public CompletableFuture acquire(Hash hash) { return acquire(hash, Stores.current()); } /** * Attempts to acquire a complete persistent data structure for the given hash * from the connected peer. Uses the store provided as a destination. * * @param hash Hash of value to acquire. * @param store Store to acquire the persistent data to. * * @return Future for the Cell being acquired. May fail exceptionally or timeout * if the given data cannot be acquired (most likely missing from the * peer's store) */ public abstract CompletableFuture acquire(Hash hash, AStore store); /** * Request status using a sync operation. This request will automatically get * any missing data with the status request * * @param timeoutMillis Milliseconds to wait for request timeout * @return Status Vector from target Peer * * @throws IOException If an IO Error occurs * @throws TimeoutException If operation times out * */ public Result requestStatusSync(long timeoutMillis) throws IOException, TimeoutException { Future statusFuture = requestStatus(); try { return statusFuture.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new Error("Unable to get network status ", e); } } /** * Submits a status request to the Convex network peer, returning a Future once * the request has been successfully queued. * * @return A Future for the result of the requestStatus */ public abstract CompletableFuture requestStatus(); /** * Method to start waiting for a complete result. Should be called with lock on * `awaiting` map to prevent risk of missing results before it is called. * * @param id ID of result message to await * @return */ protected CompletableFuture awaitResult(long id) { CompletableFuture cf = new CompletableFuture(); awaiting.put(id, cf); return cf; } /** * Request a challenge. This is request is made by any peer that needs to find * out if another peer can be trusted. * * @param data Signed data to send to the peer for the challenge. * * @return A Future for the result of the requestChallenge * * @throws IOException if the connection fails. * */ public abstract CompletableFuture requestChallenge(SignedData data) throws IOException; /** * Submits a query to the Convex network, returning a Future once the query has * been successfully queued. * * @param query Query to execute, as a Form or Op * @param address Address to use for the query * @return A Future for the result of the query * @throws IOException If the connection is broken, or the send buffer is full */ public abstract CompletableFuture query(ACell query, Address address) throws IOException; /** * Executes a query synchronously and waits for the Result * * @param query Query to execute. Map be a form or Op * @return Result of synchronous query * @throws TimeoutException If the synchronous request timed out * @throws IOException In case of network error */ public Result querySync(ACell query) throws TimeoutException, IOException { return querySync(query, getAddress()); } /** * Executes a query synchronously and waits for the Result * * @param query Query to execute, as a String that contains one or more readable * forms. Multiple forms will be wrapped in a `do` block * @return Result of synchronous query * @throws TimeoutException If the synchronous request timed out * @throws IOException In case of network error */ public Result querySync(String query) throws TimeoutException, IOException { ACell form = buildCodeForm(query); return querySync(form, getAddress()); } /** * Executes a query synchronously and waits for the Result * * @param timeoutMillis Timeout to wait for query result. Will throw * TimeoutException if not received in this time * @param query Query to execute, as a Form or Op * @return Result of query * @throws TimeoutException If the synchronous request timed out * @throws IOException In case of network error */ public Result querySync(ACell query, long timeoutMillis) throws IOException, TimeoutException { return querySync(query, getAddress(), timeoutMillis); } /** * Executes a query synchronously and waits for the Result * * @param address Address to use for the query * @param query Query to execute, as a Form or Op * @return Result of query * @throws TimeoutException If the synchronous request timed out * @throws IOException In case of network error */ public Result querySync(ACell query, Address address) throws IOException, TimeoutException { return querySync(query, address, timeout); } /** * Executes a query synchronously and waits for the Result * * @param timeoutMillis Timeout to wait for query result. Will throw * TimeoutException if not received in this time * @param address Address to use for the query * @param query Query to execute, as a Form or Op * @return Result of query * @throws TimeoutException If the synchronous request timed out * @throws IOException In case of network error */ public Result querySync(ACell query, Address address, long timeoutMillis) throws TimeoutException, IOException { Future cf = query(query, address); Result result; try { result = cf.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException e) { throw new Error("Not possible? Since there is no Thread for the future....", e); } return result; } /** * Returns the current AcountKey for the client using the API. * * @return AcountKey instance */ public AccountKey getAccountKey() { return keyPair.getAccountKey(); } /** * Returns the current Address for the client using the API. * * @return Address instance */ public Address getAddress() { return address; } /** * Disconnects the client from the network, releasing any connection resources. */ public abstract void close(); @Override public void finalize() { close(); } /** * Determines if this Client is configured to automatically generate sequence * numbers * * @return */ protected boolean isAutoSequence() { return autoSequence; } /** * Configures auto-generation of sequence numbers * * @param autoSequence true to enable auto-sequencing, false otherwise */ protected void setAutoSequence(boolean autoSequence) { this.autoSequence = autoSequence; } public Long getBalance(Address address) throws IOException { try { Future future = query(Lists.of(Symbols.BALANCE, address)); Result result = future.get(timeout, TimeUnit.MILLISECONDS); if (result.isError()) throw new Error(result.toString()); CVMLong bal = (CVMLong) result.getValue(); return bal.longValue(); } catch (ExecutionException | InterruptedException | TimeoutException ex) { throw new IOException("Unable to query balance", ex); } } /** * Connect to a local Server, using given address and keypair * * @param server Server to connect to * @param address Address to use * @param keyPair Keypair to use * @return New Client Connection */ public static ConvexLocal connect(Server server, Address address, AKeyPair keyPair) { return ConvexLocal.create(server, address, keyPair); } /** * Gets the consensus state from the remote Peer * * @return Future for consensus state * @throws TimeoutException If initial status request times out */ public abstract CompletableFuture acquireState() throws TimeoutException; }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy