forked from Qortal/q-fund-v2
192 lines
7.6 KiB
Java
192 lines
7.6 KiB
Java
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));
|
||
}
|
||
}
|