forked from Qortal/qortal
Initial APIs and core support for Q-Apps
This commit is contained in:
parent
4232616a5f
commit
32c2f68cb1
401
Q-Apps.md
Normal file
401
Q-Apps.md
Normal file
@ -0,0 +1,401 @@
|
||||
# Qortal Project - Q-Apps Documentation
|
||||
|
||||
## Introduction
|
||||
|
||||
Q-Apps are static web apps written in javascript, HTML, CSS, and other static assets. The key difference between a Q-App and a fully static site is its ability to interact with both the logged-in user and on-chain data. This is achieved using the API described in this document.
|
||||
|
||||
|
||||
|
||||
## Making a request
|
||||
|
||||
Qortal core will automatically inject a `qortalRequest()` javascript function (a Promise) to all websites/apps. This can be used to fetch or publish data to or from the Qortal blockchain. This functionality supports async/await, as well as try/catch error handling.
|
||||
|
||||
```
|
||||
async function myfunction() {
|
||||
try {
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_DATA",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
console.log(JSON.stringify(res)); // Log the response to the console
|
||||
|
||||
} catch(e) {
|
||||
console.log("Error: " + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
myfunction();
|
||||
```
|
||||
|
||||
## Timeouts
|
||||
|
||||
By default, all requests will timeout after 10 seconds, and will throw an error - `The request timed out`. If you need a longer timeout - e.g. when fetching large QDN resources that may take a long time to be retried, you can use `qortalRequestWithTimeout(request, timeout)` as an alternative to `qortalRequest(request)`.
|
||||
|
||||
```
|
||||
async function myfunction() {
|
||||
try {
|
||||
let timeout = 60000; // 60 seconds
|
||||
let res = await qortalRequestWithTimeout({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar"
|
||||
}, timeout);
|
||||
|
||||
// Do something with the avatar here
|
||||
|
||||
} catch(e) {
|
||||
console.log("Error: " + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
myfunction();
|
||||
```
|
||||
|
||||
## Supported methods
|
||||
|
||||
Here is a list of currently supported methods:
|
||||
- GET_ACCOUNT_DATA
|
||||
- GET_ACCOUNT_NAMES
|
||||
- GET_NAME_DATA
|
||||
- SEARCH_QDN_RESOURCES
|
||||
- GET_QDN_RESOURCE_STATUS
|
||||
- FETCH_QDN_RESOURCE
|
||||
- PUBLISH_QDN_RESOURCE
|
||||
- GET_WALLET_BALANCE
|
||||
- GET_BALANCE
|
||||
- SEND_COIN
|
||||
- SEARCH_CHAT_MESSAGES
|
||||
- SEND_CHAT_MESSAGE
|
||||
- LIST_GROUPS
|
||||
- JOIN_GROUP
|
||||
- DEPLOY_AT
|
||||
- GET_AT
|
||||
- GET_AT_DATA
|
||||
|
||||
More functionality will be added in the future.
|
||||
|
||||
## Example Requests
|
||||
|
||||
Here is some example requests for each of the above:
|
||||
|
||||
### Get account data
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_DATA",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Get names owned by account
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Get name data
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: "QortalDemo"
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Search QDN resources
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar", // Optional
|
||||
default: true, // Optional
|
||||
nameListFilter: "FollowedNames", // Optional
|
||||
includeStatus: false,
|
||||
includeMetadata: false,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch QDN single file resource
|
||||
Data is returned in the base64 format
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar", // Optional. If omitted, the default resource is returned, or you can alternatively use the keyword "default"
|
||||
rebuild: false
|
||||
});
|
||||
```
|
||||
|
||||
### Fetch file from multi file QDN resource
|
||||
Data is returned in the base64 format
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: "QortalDemo",
|
||||
service: "WEBSITE",
|
||||
identifier: "default", // Optional. If omitted, the default resource is returned, or you can alternatively request that using the keyword "default", as shown here
|
||||
filepath: "index.html", // Required only for resources containing more than one file
|
||||
rebuild: false
|
||||
});
|
||||
```
|
||||
|
||||
### Get QDN resource status
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_STATUS",
|
||||
name: "QortalDemo",
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar" // Optional
|
||||
});
|
||||
```
|
||||
|
||||
### Publish QDN resource
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: "Demo", // Publisher must own the registered name - use GET_ACCOUNT_NAMES for a list
|
||||
service: "WEBSITE",
|
||||
data64: "base64_encoded_data",
|
||||
title: "Title",
|
||||
description: "Description",
|
||||
category: "TECHNOLOGY",
|
||||
tags: ["tag1", "tag2", "tag3", "tag4", "tag5"]
|
||||
});
|
||||
```
|
||||
|
||||
### Get wallet balance (QORT)
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "GET_WALLET_BALANCE",
|
||||
coin: "QORT"
|
||||
});
|
||||
```
|
||||
|
||||
### Get wallet balance (foreign coin)
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "GET_WALLET_BALANCE",
|
||||
coin: "LTC"
|
||||
});
|
||||
```
|
||||
|
||||
### Get address or asset balance
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_BALANCE",
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_BALANCE",
|
||||
assetId: 1,
|
||||
address: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2"
|
||||
});
|
||||
```
|
||||
|
||||
### Send coin to address
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "QORT",
|
||||
destinationAddress: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2",
|
||||
amount: 100000000, // 1 QORT
|
||||
fee: 10000 // 0.0001 QORT
|
||||
});
|
||||
```
|
||||
|
||||
### Send coin to address
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "LTC",
|
||||
destinationAddress: "LSdTvMHRm8sScqwCi6x9wzYQae8JeZhx6y",
|
||||
amount: 100000000, // 1 LTC
|
||||
fee: 20 // 0.00000020 LTC per byte
|
||||
});
|
||||
```
|
||||
|
||||
### Search or list chat messages
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "SEARCH_CHAT_MESSAGES",
|
||||
before: 999999999999999,
|
||||
after: 0,
|
||||
txGroupId: 0, // Optional (must specify either txGroupId or two involving addresses)
|
||||
// involving: ["QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2", "QSefrppsDCsZebcwrqiM1gNbWq7YMDXtG2"], // Optional (must specify either txGroupId or two involving addresses)
|
||||
// reference: "reference", // Optional
|
||||
// chatReference: "chatreference", // Optional
|
||||
// hasChatReference: true, // Optional
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Send a group chat message
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "SEND_CHAT_MESSAGE",
|
||||
groupId: 0,
|
||||
message: "Test"
|
||||
});
|
||||
```
|
||||
|
||||
### Send a private chat message
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "SEND_CHAT_MESSAGE",
|
||||
destinationAddress: "QZLJV7wbaFyxaoZQsjm6rb9MWMiDzWsqM2",
|
||||
message: "Test"
|
||||
});
|
||||
```
|
||||
|
||||
### List groups
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LIST_GROUPS",
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
### Join a group
|
||||
_Requires user approval_
|
||||
```
|
||||
await qortalRequest({
|
||||
action: "JOIN_GROUP",
|
||||
groupId: 100
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Deploy an AT
|
||||
_Requires user approval_
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "DEPLOY_AT",
|
||||
creationBytes: "12345",
|
||||
name: "test name",
|
||||
description: "test description",
|
||||
type: "test type",
|
||||
tags: "test tags",
|
||||
amount: 100000000, // 1 QORT
|
||||
assetId: 0,
|
||||
fee: 20000 // 0.0002 QORT
|
||||
});
|
||||
```
|
||||
|
||||
### Get AT info
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_AT",
|
||||
atAddress: "ASRUsCjk6fa5bujv3oWYmWaVqNtvxydpPH"
|
||||
});
|
||||
```
|
||||
|
||||
### Get AT data bytes (base58 encoded)
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "GET_AT_DATA",
|
||||
atAddress: "ASRUsCjk6fa5bujv3oWYmWaVqNtvxydpPH"
|
||||
});
|
||||
```
|
||||
|
||||
### List ATs by functionality
|
||||
```
|
||||
let res = await qortalRequest({
|
||||
action: "LIST_ATS",
|
||||
codeHash58: "4KdJETRAdymE7dodDmJbf5d9L1bp4g5Nxky8m47TBkvA",
|
||||
isExecutable: true,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Sample App
|
||||
|
||||
Here is a sample application to display the logged-in user's avatar:
|
||||
```
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
try {
|
||||
// Get QORT address of logged in account
|
||||
let address = await qortalRequest({
|
||||
action: "GET_ACCOUNT_ADDRESS"
|
||||
});
|
||||
console.log("address: " + address);
|
||||
|
||||
// Get names owned by this account
|
||||
let names = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address
|
||||
});
|
||||
console.log("names: " + JSON.stringify(names));
|
||||
|
||||
if (names.size == 0) {
|
||||
console.log("User has no registered names");
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the avatar of the first registered name
|
||||
let avatar = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: names[0].name,
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar"
|
||||
});
|
||||
console.log("avatar: " + JSON.stringify(avatar));
|
||||
|
||||
// Display the avatar image on the screen
|
||||
document.getElementsById("avatar").src = "data:image/png;base64," + avatar;
|
||||
|
||||
} catch(e) {
|
||||
console.log("Error: " + JSON.stringify(e));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<img width="500" id="avatar" />
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
## Testing and Development
|
||||
|
||||
Publishing an in-development app to mainnet isn't recommended. There are several options for developing and testing a Q-app before publishing to mainnet:
|
||||
|
||||
### Preview mode
|
||||
|
||||
All read-only operations can be tested using preview mode. It can be used as follows:
|
||||
|
||||
1. Ensure Qortal core is running locally on the machine you are developing on. Previewing via a remote node is not currently possible.
|
||||
2. Make a local API call to `POST /render/preview`, passing in the API key (found in apikey.txt), and the path to the root of your Q-App, for example:
|
||||
```
|
||||
curl -X POST "http://localhost:12391/render/preview" -H "X-API-KEY: apiKeyGoesHere" -d "/home/username/Websites/MyApp"
|
||||
```
|
||||
3. This returns a URL, which can be copied and pasted into a browser to view the preview
|
||||
4. Modify the Q-App as required, then repeat from step 2 to generate a new preview URL
|
||||
|
||||
This is a short term method until preview functionality has been implemented within the UI.
|
||||
|
||||
|
||||
### Single node testnet
|
||||
|
||||
For full read/write testing of a Q-App, you can set up a single node testnet (often referred to as devnet) on your local machine. See [Single Node Testnet Quick Start Guide](TestNets.md#quick-start).
|
@ -110,6 +110,7 @@ Your options are:
|
||||
}
|
||||
```
|
||||
|
||||
<a name="quick-start"></a>
|
||||
## Quick start
|
||||
Here are some steps to quickly get a single node testnet up and running with a generic minting account:
|
||||
1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
|
||||
|
@ -53,7 +53,7 @@ public class ApiService {
|
||||
|
||||
private ApiService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource");
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.apps.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@ -37,7 +37,7 @@ public class GatewayService {
|
||||
|
||||
private GatewayService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.gateway.resource");
|
||||
this.config.packages("org.qortal.api.gateway.resource", "org.qortal.api.apps.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
|
@ -25,6 +25,10 @@ public class HTMLParser {
|
||||
String baseUrl = this.linkPrefix + "/";
|
||||
Elements head = document.getElementsByTag("head");
|
||||
if (!head.isEmpty()) {
|
||||
// Add q-apps script tag
|
||||
String qAppsScriptElement = String.format("<script src=\"/apps/q-apps.js?time=%d\">", System.currentTimeMillis());
|
||||
head.get(0).prepend(qAppsScriptElement);
|
||||
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s\">", baseUrl);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
210
src/main/java/org/qortal/api/apps/resource/AppsResource.java
Normal file
210
src/main/java/org/qortal/api/apps/resource/AppsResource.java
Normal file
@ -0,0 +1,210 @@
|
||||
package org.qortal.api.apps.resource;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.arbitrary.apps.QApp;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Path("/apps")
|
||||
@Tag(name = "Apps")
|
||||
public class AppsResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
@GET
|
||||
@Path("/q-apps.js")
|
||||
@Hidden // For internal Q-App API use only
|
||||
@Operation(
|
||||
summary = "Javascript interface for Q-Apps",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "javascript",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getQAppsJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/q-apps-helper.js")
|
||||
@Hidden // For testing only
|
||||
public String getQAppsHelperJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps-helper.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/account")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public AccountData getAccount(@QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getAccountData(address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/account/names")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<NameData> getAccountNames(@QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getAccountNames(address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/name")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public NameData getName(@QueryParam("name") String name) {
|
||||
try {
|
||||
return QApp.getNameData(name);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/chatmessages")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ChatMessage> searchChatMessages(@QueryParam("before") Long before, @QueryParam("after") Long after, @QueryParam("txGroupId") Integer txGroupId, @QueryParam("involving") List<String> involvingAddresses, @QueryParam("reference") String reference, @QueryParam("chatReference") String chatReference, @QueryParam("hasChatReference") Boolean hasChatReference, @QueryParam("limit") Integer limit, @QueryParam("offset") Integer offset, @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.searchChatMessages(before, after, txGroupId, involvingAddresses, reference, chatReference, hasChatReference, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resources")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ArbitraryResourceInfo> getResources(@QueryParam("service") Service service, @QueryParam("identifier") String identifier, @Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource, @Parameter(description = "Filter names by list") @QueryParam("nameListFilter") String nameListFilter, @Parameter(description = "Include status") @QueryParam("includeStatus") Boolean includeStatus, @Parameter(description = "Include metadata") @QueryParam("includeMetadata") Boolean includeMetadata, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.searchQdnResources(service, identifier, defaultResource, nameListFilter, includeStatus, includeMetadata, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resourcestatus")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public ArbitraryResourceStatus getResourceStatus(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier) {
|
||||
return QApp.getQdnResourceStatus(service, name, identifier);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public String getResource(@QueryParam("service") Service service, @QueryParam("name") String name, @QueryParam("identifier") String identifier, @QueryParam("filepath") String filepath, @QueryParam("rebuild") boolean rebuild) {
|
||||
try {
|
||||
return QApp.fetchQdnResource64(service, name, identifier, filepath, rebuild);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/groups")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<GroupData> listGroups(@Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.listGroups(limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/balance")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public Long getBalance(@QueryParam("assetId") long assetId, @QueryParam("address") String address) {
|
||||
try {
|
||||
return QApp.getBalance(assetId, address);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/at")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public ATData getAT(@QueryParam("atAddress") String atAddress) {
|
||||
try {
|
||||
return QApp.getAtInfo(atAddress);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/atdata")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public String getATData(@QueryParam("atAddress") String atAddress) {
|
||||
try {
|
||||
return QApp.getAtData58(atAddress);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/ats")
|
||||
@Hidden // For internal Q-App API use only
|
||||
public List<ATData> listATs(@QueryParam("codeHash58") String codeHash58, @QueryParam("isExecutable") Boolean isExecutable, @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
try {
|
||||
return QApp.listATs(codeHash58, isExecutable, limit, offset, reverse);
|
||||
} catch (DataException | IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -128,10 +128,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@ -175,10 +175,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@ -232,10 +232,10 @@ public class ArbitraryResource {
|
||||
.getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse);
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
creatorName.resources = resources;
|
||||
@ -511,10 +511,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@ -1258,42 +1258,4 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
}
|
||||
|
276
src/main/java/org/qortal/arbitrary/apps/QApp.java
Normal file
276
src/main/java/org/qortal/arbitrary/apps/QApp.java
Normal file
@ -0,0 +1,276 @@
|
||||
package org.qortal.arbitrary.apps;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.group.GroupData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class QApp {
|
||||
|
||||
public static AccountData getAccountData(String address) throws DataException {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getAccountRepository().getAccount(address);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<NameData> getAccountNames(String address) throws DataException {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getNameRepository().getNamesByOwner(address);
|
||||
}
|
||||
}
|
||||
|
||||
public static NameData getNameData(String name) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
return LiteNode.getInstance().fetchNameData(name);
|
||||
} else {
|
||||
return repository.getNameRepository().fromName(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ChatMessage> searchChatMessages(Long before, Long after, Integer txGroupId, List<String> involvingAddresses,
|
||||
String reference, String chatReference, Boolean hasChatReference,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId == null && involvingAddresses.size() != 2)
|
||||
|| (txGroupId != null && !involvingAddresses.isEmpty()))
|
||||
throw new IllegalArgumentException("Invalid txGroupId or involvingAddresses");
|
||||
|
||||
// Check any provided addresses are valid
|
||||
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
|
||||
throw new IllegalArgumentException("Invalid address");
|
||||
|
||||
if (before != null && before < 1500000000000L)
|
||||
throw new IllegalArgumentException("Invalid timestamp");
|
||||
|
||||
byte[] referenceBytes = null;
|
||||
if (reference != null)
|
||||
referenceBytes = Base58.decode(reference);
|
||||
|
||||
byte[] chatReferenceBytes = null;
|
||||
if (chatReference != null)
|
||||
chatReferenceBytes = Base58.decode(chatReference);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
before,
|
||||
after,
|
||||
txGroupId,
|
||||
referenceBytes,
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
limit, offset, reverse);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> searchQdnResources(Service service, String identifier, Boolean defaultResource,
|
||||
String nameListFilter, Boolean includeStatus, Boolean includeMetadata,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Treat empty identifier as null
|
||||
if (identifier != null && identifier.isEmpty()) {
|
||||
identifier = null;
|
||||
}
|
||||
|
||||
// Ensure that "default" and "identifier" parameters cannot coexist
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
if (defaultRes == true && identifier != null) {
|
||||
throw new IllegalArgumentException("identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
// Load filter from list if needed
|
||||
List<String> names = null;
|
||||
if (nameListFilter != null) {
|
||||
names = ResourceListManager.getInstance().getStringsInList(nameListFilter);
|
||||
if (names.isEmpty()) {
|
||||
// List doesn't exist or is empty - so there will be no matches
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static ArbitraryResourceStatus getQdnResourceStatus(Service service, String name, String identifier) {
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, false);
|
||||
}
|
||||
|
||||
public static String fetchQdnResource64(Service service, String name, String identifier, String filepath, boolean rebuild) throws DataException {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
|
||||
int attempts = 0;
|
||||
int maxAttempts = 5;
|
||||
|
||||
// Loop until we have data
|
||||
while (!Controller.isStopping()) {
|
||||
attempts++;
|
||||
if (!arbitraryDataReader.isBuilding()) {
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(rebuild);
|
||||
break;
|
||||
} catch (MissingDataException e) {
|
||||
if (attempts > maxAttempts) {
|
||||
// Give up after 5 attempts
|
||||
throw new DataException("Data unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread.sleep(3000L);
|
||||
}
|
||||
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
// Assume the resource doesn't exist
|
||||
throw new DataException("File not found");
|
||||
}
|
||||
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
// This is a single file resource
|
||||
filepath = files[0];
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("filepath is required for resources containing more than one file");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: limit file size that can be read into memory
|
||||
java.nio.file.Path path = Paths.get(outputPath.toString(), filepath);
|
||||
if (!Files.exists(path)) {
|
||||
return null;
|
||||
}
|
||||
byte[] bytes = Files.readAllBytes(path);
|
||||
if (bytes != null) {
|
||||
return Base64.toBase64String(bytes);
|
||||
}
|
||||
throw new DataException("File contents could not be read");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new DataException(String.format("Unable to fetch resource: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public static List<GroupData> listGroups(Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<GroupData> allGroupData = repository.getGroupRepository().getAllGroups(limit, offset, reverse);
|
||||
allGroupData.forEach(groupData -> {
|
||||
try {
|
||||
groupData.memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupId());
|
||||
} catch (DataException e) {
|
||||
// Exclude memberCount for this group
|
||||
}
|
||||
});
|
||||
return allGroupData;
|
||||
}
|
||||
}
|
||||
|
||||
public static Long getBalance(Long assetId, String address) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (assetId == null)
|
||||
assetId = Asset.QORT;
|
||||
|
||||
Account account = new Account(repository, address);
|
||||
return account.getConfirmedBalance(assetId);
|
||||
}
|
||||
}
|
||||
|
||||
public static ATData getAtInfo(String atAddress) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atAddress);
|
||||
if (atData == null) {
|
||||
throw new IllegalArgumentException("AT not found");
|
||||
}
|
||||
return atData;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAtData58(String atAddress) throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
if (atStateData == null) {
|
||||
throw new IllegalArgumentException("AT not found");
|
||||
}
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
return Base58.encode(dataBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ATData> listATs(String codeHash58, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Decode codeHash
|
||||
byte[] codeHash;
|
||||
try {
|
||||
codeHash = Base58.decode(codeHash58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
// codeHash must be present and have correct length
|
||||
if (codeHash == null || codeHash.length != 32)
|
||||
throw new IllegalArgumentException("Invalid code hash");
|
||||
|
||||
// Impose a limit on 'limit'
|
||||
if (limit != null && limit > 100)
|
||||
throw new IllegalArgumentException("Limit is too high");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,11 +3,11 @@ package org.qortal.utils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.*;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@ -440,4 +440,41 @@ public class ArbitraryTransactionUtils {
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
}
|
||||
|
206
src/main/resources/q-apps/q-apps.js
Normal file
206
src/main/resources/q-apps/q-apps.js
Normal file
@ -0,0 +1,206 @@
|
||||
function httpGet(url) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", url, false);
|
||||
request.send(null);
|
||||
return request.responseText;
|
||||
}
|
||||
|
||||
function handleResponse(event, response) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle emmpty or missing responses
|
||||
if (response == null || response.length == 0) {
|
||||
response = "{\"error\": \"Empty response\"}"
|
||||
}
|
||||
|
||||
// Parse response
|
||||
let responseObj;
|
||||
try {
|
||||
responseObj = JSON.parse(response);
|
||||
} catch (e) {
|
||||
// Not all responses will be JSON
|
||||
responseObj = response;
|
||||
}
|
||||
|
||||
// Respond to app
|
||||
if (responseObj.error != null) {
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
error: responseObj
|
||||
});
|
||||
}
|
||||
else {
|
||||
event.ports[0].postMessage({
|
||||
result: responseObj,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event == null || event.data == null || event.data.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (event.data.action == null) {
|
||||
// This could be a response from the UI
|
||||
handleResponse(event, event.data);
|
||||
}
|
||||
if (event.data.requestedHandler != null && event.data.requestedHandler === "UI") {
|
||||
// This request was destined for the UI, so ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Core received event: " + JSON.stringify(event.data));
|
||||
|
||||
let url;
|
||||
let response;
|
||||
let data = event.data;
|
||||
|
||||
switch (data.action) {
|
||||
case "GET_ACCOUNT_DATA":
|
||||
response = httpGet("/apps/account?address=" + data.address);
|
||||
break;
|
||||
|
||||
case "GET_ACCOUNT_NAMES":
|
||||
response = httpGet("/apps/account/names?address=" + data.address);
|
||||
break;
|
||||
|
||||
case "GET_NAME_DATA":
|
||||
response = httpGet("/apps/name?name=" + data.name);
|
||||
break;
|
||||
|
||||
case "SEARCH_QDN_RESOURCES":
|
||||
url = "/apps/resources?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
if (data.default != null) url = url.concat("&default=" + data.default);
|
||||
if (data.nameListFilter != null) url = url.concat("&nameListFilter=" + data.nameListFilter);
|
||||
if (data.includeStatus != null) url = url.concat("&includeStatus=" + new Boolean(data.includeStatus).toString());
|
||||
if (data.includeMetadata != null) url = url.concat("&includeMetadata=" + new Boolean(data.includeMetadata).toString());
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "FETCH_QDN_RESOURCE":
|
||||
url = "/apps/resource?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.name != null) url = url.concat("&name=" + data.name);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
if (data.filepath != null) url = url.concat("&filepath=" + data.filepath);
|
||||
if (data.rebuild != null) url = url.concat("&rebuild=" + new Boolean(data.rebuild).toString())
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_QDN_RESOURCE_STATUS":
|
||||
url = "/apps/resourcestatus?";
|
||||
if (data.service != null) url = url.concat("&service=" + data.service);
|
||||
if (data.name != null) url = url.concat("&name=" + data.name);
|
||||
if (data.identifier != null) url = url.concat("&identifier=" + data.identifier);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "SEARCH_CHAT_MESSAGES":
|
||||
url = "/apps/chatmessages?";
|
||||
if (data.before != null) url = url.concat("&before=" + data.before);
|
||||
if (data.after != null) url = url.concat("&after=" + data.after);
|
||||
if (data.txGroupId != null) url = url.concat("&txGroupId=" + data.txGroupId);
|
||||
if (data.involving != null) data.involving.forEach((x, i) => url = url.concat("&involving=" + x));
|
||||
if (data.reference != null) url = url.concat("&reference=" + data.reference);
|
||||
if (data.chatReference != null) url = url.concat("&chatReference=" + data.chatReference);
|
||||
if (data.hasChatReference != null) url = url.concat("&hasChatReference=" + new Boolean(data.hasChatReference).toString());
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "LIST_GROUPS":
|
||||
url = "/apps/groups?";
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_BALANCE":
|
||||
url = "/apps/balance?";
|
||||
if (data.assetId != null) url = url.concat("&assetId=" + data.assetId);
|
||||
if (data.address != null) url = url.concat("&address=" + data.address);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_AT":
|
||||
url = "/apps/at?";
|
||||
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "GET_AT_DATA":
|
||||
url = "/apps/atdata?";
|
||||
if (data.atAddress != null) url = url.concat("&atAddress=" + data.atAddress);
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
case "LIST_ATS":
|
||||
url = "/apps/ats?";
|
||||
if (data.codeHash58 != null) url = url.concat("&codeHash58=" + data.codeHash58);
|
||||
if (data.isExecutable != null) url = url.concat("&isExecutable=" + data.isExecutable);
|
||||
if (data.limit != null) url = url.concat("&limit=" + data.limit);
|
||||
if (data.offset != null) url = url.concat("&offset=" + data.offset);
|
||||
if (data.reverse != null) url = url.concat("&reverse=" + new Boolean(data.reverse).toString());
|
||||
response = httpGet(url);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Pass to parent (UI), in case they can fulfil this request
|
||||
event.data.requestedHandler = "UI";
|
||||
parent.postMessage(event.data, '*', [event.ports[0]]);
|
||||
return;
|
||||
}
|
||||
|
||||
handleResponse(event, response);
|
||||
|
||||
}, false);
|
||||
|
||||
const awaitTimeout = (timeout, reason) =>
|
||||
new Promise((resolve, reject) =>
|
||||
setTimeout(
|
||||
() => (reason === undefined ? resolve() : reject(reason)),
|
||||
timeout
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with no timeout
|
||||
*/
|
||||
const qortalRequestWithNoTimeout = (request) => new Promise((res, rej) => {
|
||||
const channel = new MessageChannel();
|
||||
|
||||
channel.port1.onmessage = ({data}) => {
|
||||
channel.port1.close();
|
||||
|
||||
if (data.error) {
|
||||
rej(data.error);
|
||||
} else {
|
||||
res(data.result);
|
||||
}
|
||||
};
|
||||
|
||||
window.postMessage(request, '*', [channel.port2]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with the default timeout (10 seconds)
|
||||
*/
|
||||
const qortalRequest = (request) =>
|
||||
Promise.race([qortalRequestWithNoTimeout(request), awaitTimeout(10000, "The request timed out")]);
|
||||
|
||||
/**
|
||||
* Make a Qortal (Q-Apps) request with a custom timeout, specified in milliseconds
|
||||
*/
|
||||
const qortalRequestWithTimeout = (request, timeout) =>
|
||||
Promise.race([qortalRequestWithNoTimeout(request), awaitTimeout(timeout, "The request timed out")]);
|
Loading…
Reference in New Issue
Block a user