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 "); } 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)); } }