Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20e462ec03 |
@@ -45,10 +45,18 @@ import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
// interacting over a JNI boundary with off-heap memory (via java.nio.ByteBuffer)
|
||||
// and aggressive memory sanitation is necessary.
|
||||
//
|
||||
// WARNING:
|
||||
// Do NOT compile this code with compiler optimizations turned on. It will remove the memory
|
||||
// protection functionality entirely as it is "no impact" code from the perspective of
|
||||
// performance optimization. Configure your build system to deliberately turn off optimizing
|
||||
// profilers for this module. You will need to do the same for any C code you might interface
|
||||
// with over JNI as well. Constant time execution, memory wiping, etc. are all viewed as
|
||||
// no-impact busywork or pessimization from the perspective of a performance profiler.
|
||||
//
|
||||
// References:
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
// everything (and everyone!) referenced on the lib25519 page deserves a mention.
|
||||
// Of special mention, of course, is SUPERCOP.
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
// everything (and everyone!) referenced on the lib25519 page deserves a mention.
|
||||
// Of special mention, of course, is SUPERCOP.
|
||||
//
|
||||
// tink-java: https://github.com/tink-crypto/tink-java
|
||||
// lib25519 : https://lib25519.cr.yp.to/people.html
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.encoding;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.encoding;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.encoding;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.tools;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.tools;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import swiss.qpq.gajumaru.core.encoding.ZJ;
|
||||
|
||||
|
||||
// NodeClient provides a simple interface to query Gajumaru nodes.
|
||||
// Ported from hz.erl
|
||||
//
|
||||
// Conceptually very similar to Hakuzaru's main interface, but (at least currently)
|
||||
// more limited (no contract deployment, call, etc.). The most obvious difference is that
|
||||
// instead of gm-java being a stateful service the way hz can be, this is a pure library
|
||||
// and the caller is expected to be the one that is keeping track of a list of possible
|
||||
// endpoints.
|
||||
//
|
||||
// This design will evolve in the future as the concept of network and inter-network
|
||||
// crawling evolved (and obviously as ACs are introduced). In this iteration, though,
|
||||
// my goal is to keep things as simple as possible, and that is why this is probably
|
||||
// somewhat unidiomatic for Java (I don't *really* know, but this works).
|
||||
|
||||
public final class NodeClient {
|
||||
|
||||
public static final class Endpoint {
|
||||
public final String host;
|
||||
public final int port;
|
||||
public final boolean useTls;
|
||||
|
||||
public Endpoint(String host, int port) {
|
||||
this(host, port, false);
|
||||
}
|
||||
|
||||
public Endpoint(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Endpoint> endpoints;
|
||||
private int timeout = 5000;
|
||||
|
||||
public NodeClient(List<Endpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one endpoint is required");
|
||||
}
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
public void setTimeout(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public Map<String, Object> status() throws Exception {
|
||||
return (Map<String, Object>) request("/v3/status", "GET", null);
|
||||
}
|
||||
|
||||
public long topHeight() throws Exception {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/headers/top", "GET", null);
|
||||
return ((Number) res.get("height")).longValue();
|
||||
}
|
||||
|
||||
public Map<String, Object> account(String accountId) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/accounts/" + accountId, "GET", null);
|
||||
}
|
||||
|
||||
public long nextNonce(String accountId) throws Exception {
|
||||
try {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null);
|
||||
return ((Number) res.get("next_nonce")).longValue();
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("Account not found")) {
|
||||
return 1;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> postTx(String signedTx) throws Exception {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("tx", signedTx);
|
||||
return (Map<String, Object>) request("/v3/transactions", "POST", body);
|
||||
}
|
||||
|
||||
public Map<String, Object> tx(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash, "GET", null);
|
||||
}
|
||||
|
||||
public Map<String, Object> txInfo(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash + "/info", "GET", null);
|
||||
}
|
||||
|
||||
private Object request(String path, String method, Object body) throws Exception {
|
||||
Exception lastException = null;
|
||||
for (Endpoint endpoint : endpoints) {
|
||||
try {
|
||||
if (endpoint.useTls) {
|
||||
try {
|
||||
return performRequest(endpoint, path, method, body, true);
|
||||
} catch (Exception e) {
|
||||
// Fallback to HTTP
|
||||
return performRequest(endpoint, path, method, body, false);
|
||||
}
|
||||
} else {
|
||||
return performRequest(endpoint, path, method, body, false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
throw new Exception("All endpoints failed. Last error: " + (lastException != null ? lastException.getMessage() : "unknown"), lastException);
|
||||
}
|
||||
|
||||
private Object performRequest(Endpoint endpoint, String path, String method, Object body, boolean useTls) throws Exception {
|
||||
String protocol = useTls ? "https://" : "http://";
|
||||
String urlStr = protocol + endpoint.host + ":" + endpoint.port + path;
|
||||
URL url = new URL(urlStr);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setConnectTimeout(timeout);
|
||||
conn.setReadTimeout(timeout);
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
|
||||
if (body != null) {
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input = ZJ.encode(body).getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||
code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
Object decoded = ZJ.decode(response.toString());
|
||||
if (code < 200 || code >= 300) {
|
||||
String reason = "HTTP " + code;
|
||||
if (decoded instanceof Map) {
|
||||
Object r = ((Map<?, ?>) decoded).get("reason");
|
||||
if (r != null) reason = r.toString();
|
||||
}
|
||||
throw new Exception(reason);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.tools;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import swiss.qpq.gajumaru.core.crypto.Blake2b;
|
||||
import swiss.qpq.gajumaru.core.crypto.Ed25519;
|
||||
import swiss.qpq.gajumaru.core.data.Id;
|
||||
import swiss.qpq.gajumaru.core.data.SignedTx;
|
||||
import swiss.qpq.gajumaru.core.data.SpendTx;
|
||||
import swiss.qpq.gajumaru.core.encoding.ApiEncoder;
|
||||
|
||||
// This is where the gnarly mess used to generate transactions lives for now.
|
||||
// They might not stay here, but this is where they are for now.
|
||||
|
||||
public final class TransactionService {
|
||||
|
||||
private TransactionService() {}
|
||||
|
||||
public static String buildSpendTx(
|
||||
String networkId,
|
||||
String senderId,
|
||||
String recipientId,
|
||||
BigInteger amount,
|
||||
BigInteger gasPrice,
|
||||
BigInteger gas,
|
||||
long ttl,
|
||||
long nonce,
|
||||
String payload,
|
||||
byte[] seckey
|
||||
) {
|
||||
// 1. Decode IDs
|
||||
Id sender = new Id(Id.Tag.ACCOUNT, ApiEncoder.decode(senderId).payload());
|
||||
|
||||
// Recipient can be ak_ (account) or ct_ (contract)
|
||||
ApiEncoder.DecodeResult recipientResult = ApiEncoder.decode(recipientId);
|
||||
Id.Tag recipientTag = recipientResult.type() == ApiEncoder.Type.CONTRACT_PUBKEY ? Id.Tag.CONTRACT : Id.Tag.ACCOUNT;
|
||||
Id recipient = new Id(recipientTag, recipientResult.payload());
|
||||
|
||||
// 2. Build SpendTx
|
||||
byte[] payloadBytes = payload != null ? payload.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||
SpendTx tx = new SpendTx(sender, recipient, amount, gasPrice, gas, ttl, nonce, payloadBytes);
|
||||
byte[] serializedTx = tx.serialize();
|
||||
|
||||
// 3. Prepare NetworkHash
|
||||
byte[] networkIdBytes = networkId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] txHash = Blake2b.hash(serializedTx, 32);
|
||||
byte[] networkHash = new byte[networkIdBytes.length + txHash.length];
|
||||
System.arraycopy(networkIdBytes, 0, networkHash, 0, networkIdBytes.length);
|
||||
System.arraycopy(txHash, 0, networkHash, networkIdBytes.length, txHash.length);
|
||||
|
||||
// 4. Sign
|
||||
byte[] signature = Ed25519.sign(seckey, networkHash);
|
||||
|
||||
// 5. Wrap in SignedTx
|
||||
SignedTx signedTx = new SignedTx(List.of(signature), serializedTx);
|
||||
|
||||
// 6. Encode for API
|
||||
return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user