Files
q-fund-v2/docs/Qrowdfund-v2.java
2025-08-23 19:47:40 -04:00

192 lines
7.6 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package org.qortal.at.qrowdfundv2;
import org.ciyam.at.*;
import org.qortal.crypto.Crypto;
import org.qortal.utils.Base58;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import static org.ciyam.at.OpCode.calcOffset;
/**
* Q-Fund v2 (non-refund) — pays awardee even if goal is not met.
*
* Differences vs v1 (Qrowdfund):
* - No donor refund loop; both branches pay awardee
* - Minimal program after timeout
*
* Data (ABI v2, big-endian u64 unless noted):
* [0] u64 sleepMinutes (offset 0)
* [1] u64 goalAmountAtoms (offset 8) // informational; not used to decide payout
* [2] u64 sleepUntilTimestamp (offset 16) // computed at runtime
* [3] u64 sleepUntilHeight (offset 24) // computed at runtime
* [4] u64 finalAmount (offset 32) // computed at runtime
* [5] u64 creationTimestamp (offset 40) // filled at runtime
* [6..9] 32B awardee address (offset 48)
*
* Code outline (canonical chain variant):
* GET_CREATION_TIMESTAMP -> [5]
* GET_BLOCK_TIMESTAMP -> [2]
* ADD_MINUTES_TO_TIMESTAMP [2] + [0] -> [2]
* SET_DAT [3] = [2]
* SHR_VAL [3], 32 // timestamp->height
* SLP_DAT [3]
* GET_CURRENT_BALANCE -> [4]
* BLT_DAT [4] < [1] ? goto (second payout) : fallthrough
* (first payout) SET_B_DAT [awardee]; PAY_ALL_TO_ADDRESS_IN_B; FIN_IMD
* (second payout) SET_PCS; SET_B_DAT [awardee]; PAY_ALL_TO_ADDRESS_IN_B; FIN_IMD
*/
public class QrowdfundV2 {
/**
* Returns Qortal AT creation bytes for Qrowdfund v2 AT.
*
* @param sleepMinutes Time period for allowing donations (roughly 1 block per minute)
* @param goalAmount Goal in QORT (QORT×1e8). Informational in v2.
* @param awardee Qortal address of awardee
*/
public static byte[] buildQortalAT(int sleepMinutes, long goalAmount, String awardee) {
if (sleepMinutes < 10 || sleepMinutes > 30 * 24 * 60)
throw new IllegalArgumentException("Sleep period should be between 10 minutes and 1 month");
if (goalAmount < 100_0000L || goalAmount > 1_000_000_00000000L)
throw new IllegalArgumentException("Goal amount should be between 0.01 QORT and 1,000,000 QORT");
if (!Crypto.isValidAddress(awardee))
throw new IllegalArgumentException("Awardee address should be a valid Qortal address");
// --- Data layout indices (u64 slots; index*8 = byte offset)
int idx = 0;
final int addrSleepMinutes = idx++; // 0 -> offset 0
final int addrGoalAmount = idx++; // 1 -> offset 8
final int addrCutTs = idx++; // 2 -> offset 16
final int addrCutHeight = idx++; // 3 -> offset 24
final int addrFinalAmount = idx++; // 4 -> offset 32
final int addrCreationTs = idx++; // 5 -> offset 40
final int addrAwardee = idx; idx += 4; // 6..9 -> offset 48
// --- Build data bytes
ByteBuffer data = ByteBuffer.allocate(idx * MachineState.VALUE_SIZE);
data.position(addrSleepMinutes * MachineState.VALUE_SIZE);
data.putLong(sleepMinutes);
data.position(addrGoalAmount * MachineState.VALUE_SIZE);
data.putLong(goalAmount);
data.position(addrAwardee * MachineState.VALUE_SIZE);
data.put(Base58.decode(awardee));
// --- Build code bytes (exact canonical opcode sequence)
ByteBuffer code = ByteBuffer.allocate(512);
// We'll use a two-pass approach to resolve relative jump offsets
Integer labelSecondPayout = null;
for (int pass = 0; pass < 2; ++pass) {
code.clear();
try {
// [00] creation timestamp -> [5]
code.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrCreationTs));
// [01] current block timestamp -> [2]
code.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrCutTs));
// [02] cutoff timestamp = [2] + minutes([0])
code.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrCutTs, addrCutTs, addrSleepMinutes));
// [03] height = cutoff ts >> 32
code.put(OpCode.SET_DAT.compile(addrCutHeight, addrCutTs));
code.put(OpCode.SHR_VAL.compile(addrCutHeight, 32L));
// [05] sleep until height
code.put(OpCode.SLP_DAT.compile(addrCutHeight));
// [06] final amount
code.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CURRENT_BALANCE, addrFinalAmount));
// [07] if final < goal -> jump to second payout
int jmpTarget = labelSecondPayout;
int offset = calcOffset(code, jmpTarget);
code.put(OpCode.BLT_DAT.compile(addrFinalAmount, addrGoalAmount, offset));
// [08-10] first payout path (fallthrough)
code.put(OpCode.EXT_FUN_VAL.compile(FunctionCode.SET_B_DAT, addrAwardee));
code.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B));
code.put(OpCode.FIN_IMD.compile());
// [11] label: second payout path
labelSecondPayout = code.position();
// Canonical sequence includes SET_PCS then repeats payout+finish
code.put(OpCode.SET_PCS.compile());
code.put(OpCode.EXT_FUN_VAL.compile(FunctionCode.SET_B_DAT, addrAwardee));
code.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B));
code.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT", e);
}
}
code.flip();
byte[] codeBytes = new byte[code.limit()];
code.get(codeBytes);
// Build creation bytes
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, data.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private static void usage() {
System.err.println("usage: qrowdfundv2 <timeout-minutes> <goal> <awardee-address>");
}
public static void main(String[] args) {
if (args.length != 3) {
usage();
System.exit(2);
}
int sleepMinutes;
try {
sleepMinutes = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
usage();
System.err.println();
System.err.printf("Entry window minutes '%s' invalid - should be integer larger than 10", args[0]);
System.exit(1);
// not reached
throw e;
}
long goal;
try {
goal = new BigDecimal(args[1]).setScale(8).unscaledValue().longValueExact();
} catch (Exception e) {
usage();
System.err.println();
System.err.printf("Goal '%s' invalid", args[1]);
System.exit(1);
// not reached
throw e;
}
String awardee = args[2];
if (!Crypto.isValidAddress(awardee)) {
usage();
System.err.println();
System.err.printf("Awardee address '%s' not a Qortal address", awardee);
System.exit(1);
}
byte[] creationBytes = buildQortalAT(sleepMinutes, goal, awardee);
System.out.printf("Creation bytes:\n%s\n", Base58.encode(creationBytes));
}
}