# Q‑Fund App: Dual‑AT Support Implementation Guide (v1 refund / v2 non‑refund) **Objective:** Modify the current Q‑Fund app so creators can choose between two crowdfund AT variants at creation time: - **v2 (default)** — *Pays awardee even if goal is unmet* (non‑refund). `codeHash = HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr`, `codeBytes.length = 104` - **v1 (opt‑in)** — *Refunds donors if goal is unmet* (refund). `codeHash = 9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F`, `codeBytes.length = 167` The UI adds a checkbox **“Refund Donors (if goal not reached)”** — **off by default uses v2**, **on** deploys v1. --- ## 0) Repo landmarks (assumed) - **Create flow** (today): `src/components/Crowdfund/NewCrowdfund.tsx` - **Helpers** (suggested new): `src/lib/at/crowdfund.ts` - **Server bridge** (unchanged): POST `/{at}/create` then `qortalRequest({ action:'DEPLOY_AT', ... })` > If your paths differ, adapt filenames accordingly. Keep all logic behind a single **buildCrowdfundCreationBytes(...)** helper to minimize surface area. --- ## 1) Pin canonical constants (one source of truth) Create **`src/lib/at/crowdfund.ts`** (or update an existing helper) and pin the canonical **codeBytesBase64** and ABI layouts. ```ts // src/lib/at/crowdfund.ts import bs58 from 'bs58'; // ---------- Canonical code bytes (immutable) ---------- export const CROWDFUND_V1_CODEHASH = '9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F'; export const CROWDFUND_V2_CODEHASH = 'HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr'; export const CROWDFUND_V1_CODEBYTES_BASE64 = 'NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAKMgQDKDAzAwQAAAAFNQElAAAABhsAAAAGByg1AwcAAAAFIAAAAAUAAAACCyg1AwUAAAAHJAAAAAcAAAAI0jUDBgAAAAkyAwozBAIAAAAJGgAAAFk='; // len 167 bytes export const CROWDFUND_V2_CODEBYTES_BASE64 = 'NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAGMgQDKDA4AR8AAAAAAAAABjIEAyg='; // len 104 bytes // ---------- ABI (big-endian u64 unless noted) ---------- export type Variant = 'v1_refund' | 'v2_nonrefund'; // Common fields // [0..7] u64 sleepMinutes // [8..15] u64 goalAtoms (QORT * 1e8) // v1 (refund) extras + awardee offset: export const V1_PAYMENT_TYPE_U64 = 2n; // must be written at offset 64 export const V1_AWARDEE_OFFSET_BYTES = 80; // awardee 32 bytes at [80..111] // v2 (non-refund) awardee offset: export const V2_AWARDEE_OFFSET_BYTES = 48; // awardee 32 bytes at [48..79] // ---------- Helpers ---------- function beWriteU64(buf: Uint8Array, byteOffset: number, value: bigint) { for (let i = 7; i >= 0; i--) { buf[byteOffset + (7 - i)] = Number((value >> BigInt(i * 8)) & 0xffn); } } export function qortToAtoms(qort: string | number): bigint { const s = typeof qort === 'number' ? qort.toString() : String(qort); const [intPart, fracPartRaw = ''] = s.split('.'); const fracPart = (fracPartRaw + '00000000').slice(0, 8); return BigInt(intPart) * 100000000n + BigInt(fracPart); } export function buildCrowdfundDataBytes(params: { variant: Variant; sleepMinutes: number; goalAtoms: bigint; awardeeBase58: string; }): Uint8Array { if (params.sleepMinutes < 10) throw new Error('sleepMinutes must be >= 10'); const awardee = bs58.decode(params.awardeeBase58); if (awardee.length !== 32) throw new Error('awardee must decode to 32 bytes'); let totalBytes: number; let awardeeOffset: number; if (params.variant === 'v1_refund') { totalBytes = 112; // v1 uses fields through awardee[80..111] awardeeOffset = V1_AWARDEE_OFFSET_BYTES; } else { totalBytes = 80; // v2 uses awardee[48..79] awardeeOffset = V2_AWARDEE_OFFSET_BYTES; } const data = new Uint8Array(totalBytes); beWriteU64(data, 0, BigInt(params.sleepMinutes)); // [0..7] beWriteU64(data, 8, params.goalAtoms); // [8..15] if (params.variant === 'v1_refund') beWriteU64(data, 64, V1_PAYMENT_TYPE_U64); // [64..71] = 2 data.set(awardee, awardeeOffset); // awardee 32 bytes return data; } export function selectCodeBytesBase64(variant: Variant): string { return variant === 'v1_refund' ? CROWDFUND_V1_CODEBYTES_BASE64 : CROWDFUND_V2_CODEBYTES_BASE64; } ``` **Why this matters** - `codeBytesBase64` is **immutable**, determines `codeHash`. - `dataBytes` carries **sleep, goal, and awardee** (plus v1’s paymentType constant). - Offsets differ between v1 and v2; **do not reuse** one layout for both. --- ## 2) Add the UI control (checkbox) In **`src/components/Crowdfund/NewCrowdfund.tsx`**: 1. **Form state:** add a boolean `refundIfMissed` (default `false`). 2. **UI control:** a checkbox labeled **Refund Donors (if goal not reached)**. 3. **Help tip:** “If enabled, the AT refunds each donor when the goal isn’t met before the deadline.” ```tsx // inside component const [refundIfMissed, setRefundIfMissed] = useState(false); ``` > Accessibility: Ensure the label is clickable and the checkbox is reachable via keyboard. Persist the state in your draft/session store if you have one. --- ## 3) Build `creationBytes` per selection Still in **NewCrowdfund.tsx**, replace the fixed codeBytes path with the variant‑aware builder. ```ts import bs58 from 'bs58'; import { Variant, qortToAtoms, buildCrowdfundDataBytes, selectCodeBytesBase64, CROWDFUND_V1_CODEHASH, CROWDFUND_V2_CODEHASH } from '@/lib/at/crowdfund'; async function createCrowdfundAT({ sleepMinutes, goalQort, awardee }: { sleepMinutes: number; goalQort: string; awardee: string; // Base58 Qortal address }) { const variant: Variant = refundIfMissed ? 'v1_refund' : 'v2_nonrefund'; const goalAtoms = qortToAtoms(goalQort); const dataBytes = buildCrowdfundDataBytes({ variant, sleepMinutes, goalAtoms, awardeeBase58: awardee, }); const codeBytesBase64 = selectCodeBytesBase64(variant); const dataBytesBase64 = Buffer.from(dataBytes).toString('base64'); const body = { ciyamAtVersion: 2, codeBytesBase64, dataBytesBase64, numCallStackPages: 0, numUserStackPages: 0, minActivationAmount: 0 }; const res = await fetch('/at/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error(`AT create failed: ${res.status}`); const { creationBytes } = await res.json() as { creationBytes: string }; // Optional: re-hash codeBytes inside creationBytes and assert codeHash const expected = variant === 'v1_refund' ? CROWDFUND_V1_CODEHASH : CROWDFUND_V2_CODEHASH; await assertCreationParity(creationBytes, expected); return creationBytes; } ``` **Parity check helper (recommended):** ```ts // Decode creationBytes (Base58) and verify codeHash = Base58(SHA256(codeBytes)) async function assertCreationParity(creationBytesBase58: string, expectedCodeHash: string) { const raw = bs58.decode(creationBytesBase58); let ptr = 0; // ciyamAtVersion (2 bytes) ptr += 2; // codeLen (4 bytes BE) const codeLen = (raw[ptr]<<24) | (raw[ptr+1]<<16) | (raw[ptr+2]<<8) | raw[ptr+3]; ptr += 4; const code = raw.slice(ptr, ptr + codeLen); const sha = await crypto.subtle.digest('SHA-256', code); const shaBytes = new Uint8Array(sha); const codeHash = bs58.encode(shaBytes); if (codeHash !== expectedCodeHash) { throw new Error(`codeHash mismatch: expected ${expectedCodeHash}, got ${codeHash}`); } } ``` --- ## 4) Deploy via Hub (unchanged) ```ts import { qortalRequest } from '@/lib/hub'; async function deployCrowdfund(creationBytes: string, name: string, description: string, tags: string[]) { const res = await qortalRequest({ action: 'DEPLOY_AT', creationBytes, name, description, tags: tags.join(','), amount: 0.2, assetId: 0, type: 'crowdfund' }); return res; } ``` > **Note:** Names/descriptions/tags do not affect the codeHash. Variant identity comes from the **codeBytes** only. --- ## 5) UX polish - **Dynamic help text** below the checkbox: - Off (v2): “After the deadline, all funds go to the awardee even if the goal is unmet.” - On (v1): “After the deadline, donations are refunded if the goal is unmet.” - **Preview card**: show variant label — “Payout: **Always to Awardee** (v2)” or “Payout: **Refund If Unmet** (v1)”. - **Persist last choice** in local storage; default resets to **v2**. --- ## 6) Tests (must‑have) ### Unit (pure functions) - `qortToAtoms()` — decimal edge cases (`0.00000001`, trailing zeros, large ints). - `buildCrowdfundDataBytes()` — for v1/v2: verify total length, offsets (sleep, goal), awardee placement, v1’s `paymentType` at byte 64..71 equals `2`. - `selectCodeBytesBase64()` — returns pinned Base64; hashing these Base64 → `codeHash` equals pinned values. ### Integration (creation parity) - POST `/at/create` with both variants and assert: - re‑hashed `codeBytes` matches expected `codeHash` (`9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F` or `HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr`), - data segment length meets minimum (v1 ≥ 112, v2 ≥ 80), - creation bytes round‑trip (Base58 decode/encode) preserves content. ### UI - Checkbox defaults OFF. - Toggling modifies `variant` passed to builder (spy or mock). - Form validation unchanged (awardee length=32 bytes after bs58 decode; goal≥min; sleep≥10). --- ## 7) Migration & telemetry (optional) - **Feature flag**: `crowdfund.dualAT = true`; gate the checkbox for staged rollout. - **Analytics**: capture selection counts (v1 vs v2) + failures. - **Docs**: add a short “Behavior” section to the app’s help modal. --- ## 8) Troubleshooting - **codeHash mismatch** after `/at/create` → ensure you’re using the exact Base64 below. Some bundlers auto‑wrap long strings; prefer loading them from a `.b64` asset module to avoid line-wrap edits. - **awardee invalid** → confirm Base58 decoded length is **32** bytes. - **wrong offsets** → v1 awardee at **byte 80**, v2 at **byte 48**. v1 must also write `PAYMENT` (2) at **byte 64**. --- ## Appendix A — Canonical codeBytes (Base64) **v1 (refund) — 9gS2L74FdaG3zuEeYv815xVyHkhvLguq7ZGD6pf24i8F, len 167:** ``` NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAKMgQDKDAzAwQAAAAFNQElAAAABhsAAAAGByg1AwcAAAAFIAAAAAUAAAACCyg1AwUAAAAHJAAAAAcAAAAI0jUDBgAAAAkyAwozBAIAAAAJGgAAAFk= ``` **v2 (non‑refund) — HaqJBVVr9gZqgARZ5UZd7EU9ybyvVK2fCo9sx3gMMFsr, len 104:** ``` NQMBAAAABTUDAAAAAAI3BAYAAAACAAAAAgAAAAACAAAAAwAAAAJLAAAAAwAAAAAAAAAgJQAAAAM1BAAAAAAEIAAAAAQAAAABGTgBHwAAAAAAAAAGMgQDKDA4AR8AAAAAAAAABjIEAyg= ```