qortal/src/api/AddressesResource.java
catbref 034cf5dee3 API: assets & tidying
Cleaned up responses from /addresses/* endpoints
in that some return text/plain instead of application/json.

Removed need for class-local copy of ApiErrorFactory in
AddressesResource - using getInstance() instead.

Some work still needs to be done on annotating API errors.
API error examples in API UI rendered incorrectly - swagger-ui issue?

Removed repository-accessing code from api.models.*

Added /assets/order/{orderId} for fetching info on specific asset order.

NOTE: AssetRepository.getOrdersTrades() now returns trades where order is
initiating or target. (Previously was initiating order only).
qora.assets.Order.orphan() updated to reflect above change.

block-explorer.html fixed to use new API output.
2018-12-13 12:22:46 +00:00

452 lines
16 KiB
Java

package api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.Base64;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import data.account.AccountBalanceData;
import data.account.AccountData;
import qora.account.Account;
import qora.assets.Asset;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import transform.Transformer;
@Path("addresses")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/AddressesResource")
}
)
@Tag(name = "Addresses")
public class AddressesResource {
@Context
HttpServletRequest request;
@GET
@Path("/lastreference/{address}")
@Operation(
summary = "Fetch reference for next transaction to be created by address",
description = "Returns the base64-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the base64-encoded transaction signature or \"false\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String getLastReference(@Parameter(ref = "address") @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
lastReference = account.getLastReference();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
return "false";
} else {
return Base64.getEncoder().encodeToString(lastReference);
}
}
@GET
@Path("/lastreference/{address}/unconfirmed")
@Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base64-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address:unconfirmed"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the base64-encoded transaction signature",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
lastReference = account.getUnconfirmedLastReference();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
return "false";
} else {
return Base64.getEncoder().encodeToString(lastReference);
}
}
@GET
@Path("/validate/{address}")
@Operation(
summary = "Validates the given address",
description = "Returns true/false.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET validate:address"),
@ExtensionProperty(name="summary.key", value="operation:summary"),
@ExtensionProperty(name="description.key", value="operation:description"),
})
},
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public boolean validate(@PathParam("address") String address) {
return Crypto.isValidAddress(address);
}
@GET
@Path("/generatingbalance/{address}")
@Operation(
summary = "Return the generating balance of the given address",
description = "Returns the effective balance of the given address, used in Proof-of-Stake calculationgs when generating a new block.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET generatingbalance:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the generating balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getGeneratingBalance();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/balance/{address}")
@Operation(
summary = "Returns the confirmed balance of the given address",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET balance:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(Asset.QORA);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/assetbalance/{assetid}/{address}")
@Operation(
summary = "Asset-specific balance request",
description = "Returns the confirmed balance of the given address for the given asset key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET assetbalance:assetid:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\", \"INVALID_ASSET_ID\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(assetid);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/assets/{address}")
@Operation(
summary = "All assets owned by this address",
description = "Returns the list of assets for this address, with balances.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET assets:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public List<AccountBalanceData> getAssets(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAllBalances(address);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/balance/{address}/{confirmations}")
@Operation(
summary = "Calculates the balance of the given address for the given confirmations",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET balance:address:confirmations"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
throw new UnsupportedOperationException();
}
@GET
@Path("/publickey/{address}")
@Operation(
summary = "Get public key of address",
description = "Returns the base64-encoded account public key of the given address, or \"false\" if address not known or has no public key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET publickey:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the public key",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String getPublicKey(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
if (accountData == null)
return "false";
byte[] publicKey = accountData.getPublicKey();
if (publicKey == null)
return "false";
return Base64.getEncoder().encodeToString(publicKey);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/convert/{publickey}")
@Operation(
summary = "Convert public key into address",
description = "Returns account address based on supplied public key. Expects base64-encoded, 32-byte public key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET publickey:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the address",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String fromPublicKey(@PathParam("publickey") String publicKey) {
// Decode public key
byte[] publicKeyBytes;
try {
publicKeyBytes = Base64.getDecoder().decode(publicKey);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_PUBLIC_KEY, e);
}
// Correct size for public key?
if (publicKeyBytes.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_PUBLIC_KEY);
try (final Repository repository = RepositoryManager.getRepository()) {
return Crypto.toAddress(publicKeyBytes);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
}
}
}