1// Package crowdfund is an accounting-only port of the classic Solidity2// Kickstarter-style crowdfunding contract to a gno.land realm.3//4// A creator Launches a campaign with a funding goal and a duration (in blocks).5// Backers Pledge (and may Unpledge before the deadline). After the deadline the6// creator can Claim if the goal was met, otherwise backers can Refund.7//8// No real coin moves: pledges are tracked as plain uint64 accounting, exactly9// like the mapping(address => uint) balances of the Solidity original.10package crowdfund1112import (13 "strconv"1415 "chain"16 "chain/runtime"17 "chain/runtime/unsafe"1819 "gno.land/p/nt/avl/v0"20)2122// Campaign holds the state of a single crowdfunding campaign.23type Campaign struct {24 ID int25 Creator address26 Goal uint6427 Deadline int64 // block height after which the campaign is closed28 Pledged uint64 // running total pledged29 Claimed bool // creator already claimed the funds30 pledges *avl.Tree // backer address (string) -> pledged uint6431}3233var (34 campaigns = avl.NewTree() // padded id -> *Campaign (ordered by id)35 nextID = 136)3738// key zero-pads an id so avl iteration is in numeric order.39func key(id int) string {40 s := strconv.Itoa(id)41 for len(s) < 12 {42 s = "0" + s43 }44 return s45}4647func mustGet(id int) *Campaign {48 v := campaigns.Get(key(id))49 if v == nil {50 panic("campaign not found")51 }52 return v.(*Campaign)53}5455// Launch creates a new campaign owned by the caller and returns its id.56func Launch(cur realm, goal uint64, durationBlocks int) int {57 if goal == 0 {58 panic("goal must be > 0")59 }60 if durationBlocks <= 0 {61 panic("duration must be > 0")62 }63 caller := unsafe.PreviousRealm().Address()64 id := nextID65 nextID++66 c := &Campaign{67 ID: id,68 Creator: caller,69 Goal: goal,70 Deadline: runtime.ChainHeight() + int64(durationBlocks),71 pledges: avl.NewTree(),72 }73 campaigns.Set(key(id), c)74 chain.Emit("Launch",75 "id", strconv.Itoa(id),76 "creator", caller.String(),77 "goal", strconv.FormatUint(goal, 10))78 return id79}8081// Pledge backs campaign id with amount (before the deadline).82func Pledge(cur realm, id int, amount uint64) {83 if amount == 0 {84 panic("amount must be > 0")85 }86 c := mustGet(id)87 if runtime.ChainHeight() > c.Deadline {88 panic("campaign ended")89 }90 caller := unsafe.PreviousRealm().Address()91 prev := uint64(0)92 if v := c.pledges.Get(caller.String()); v != nil {93 prev = v.(uint64)94 }95 c.pledges.Set(caller.String(), prev+amount)96 c.Pledged += amount97 chain.Emit("Pledge",98 "id", strconv.Itoa(id),99 "from", caller.String(),100 "amount", strconv.FormatUint(amount, 10))101}102103// Unpledge withdraws amount from the caller's pledge (before the deadline).104func Unpledge(cur realm, id int, amount uint64) {105 if amount == 0 {106 panic("amount must be > 0")107 }108 c := mustGet(id)109 if runtime.ChainHeight() > c.Deadline {110 panic("campaign ended")111 }112 caller := unsafe.PreviousRealm().Address()113 v := c.pledges.Get(caller.String())114 if v == nil {115 panic("nothing pledged")116 }117 have := v.(uint64)118 if amount > have {119 panic("amount exceeds pledge")120 }121 if remaining := have - amount; remaining == 0 {122 c.pledges.Remove(caller.String())123 } else {124 c.pledges.Set(caller.String(), remaining)125 }126 c.Pledged -= amount127 chain.Emit("Unpledge",128 "id", strconv.Itoa(id),129 "from", caller.String(),130 "amount", strconv.FormatUint(amount, 10))131}132133// Claim lets the creator take the funds if the goal was met after the deadline.134func Claim(cur realm, id int) {135 c := mustGet(id)136 if runtime.ChainHeight() <= c.Deadline {137 panic("campaign not ended")138 }139 caller := unsafe.PreviousRealm().Address()140 if caller != c.Creator {141 panic("only creator can claim")142 }143 if c.Pledged < c.Goal {144 panic("goal not reached")145 }146 if c.Claimed {147 panic("already claimed")148 }149 c.Claimed = true150 chain.Emit("Claim",151 "id", strconv.Itoa(id),152 "amount", strconv.FormatUint(c.Pledged, 10))153}154155// Refund returns the caller's pledge if the campaign failed after the deadline.156func Refund(cur realm, id int) {157 c := mustGet(id)158 if runtime.ChainHeight() <= c.Deadline {159 panic("campaign not ended")160 }161 if c.Pledged >= c.Goal {162 panic("campaign succeeded, no refund")163 }164 caller := unsafe.PreviousRealm().Address()165 v := c.pledges.Get(caller.String())166 if v == nil {167 panic("nothing to refund")168 }169 amount := v.(uint64)170 c.pledges.Remove(caller.String())171 c.Pledged -= amount172 chain.Emit("Refund",173 "id", strconv.Itoa(id),174 "to", caller.String(),175 "amount", strconv.FormatUint(amount, 10))176}177178// --- pure/read-only helpers (also used by Render) ---179180// pct returns the funded percentage (0..100).181func pct(pledged, goal uint64) int {182 if goal == 0 {183 return 0184 }185 p := int(pledged * 100 / goal)186 if p > 100 {187 p = 100188 }189 return p190}191192// progressBar renders a fixed-width ▓░ bar for pledged/goal.193func progressBar(pledged, goal uint64) string {194 const width = 20195 filled := 0196 if goal > 0 {197 filled = int(pledged * width / goal)198 if filled > width {199 filled = width200 }201 }202 bar := ""203 for i := 0; i < width; i++ {204 if i < filled {205 bar += "▓"206 } else {207 bar += "░"208 }209 }210 return bar211}212213// state describes a campaign relative to a given block height.214func state(c *Campaign, height int64) string {215 if height <= c.Deadline {216 return "Active"217 }218 if c.Pledged >= c.Goal {219 if c.Claimed {220 return "Funded (claimed)"221 }222 return "Funded"223 }224 return "Failed"225}226227// Render shows all campaigns with a progress bar, goal, pledged and state.228func Render(path string) string {229 if campaigns.Size() == 0 {230 return "# Crowdfund\n\nNo campaigns yet. Call `Launch(goal, durationBlocks)` to start one.\n"231 }232 height := runtime.ChainHeight()233 out := "# Crowdfund\n\n"234 out += "Current block height: **" + strconv.FormatInt(height, 10) + "**\n\n"235 campaigns.Iterate("", "", func(k string, v any) bool {236 c := v.(*Campaign)237 out += "## Campaign #" + strconv.Itoa(c.ID) + "\n\n"238 out += "`" + progressBar(c.Pledged, c.Goal) + "` " + strconv.Itoa(pct(c.Pledged, c.Goal)) + "%\n\n"239 out += "- State: **" + state(c, height) + "**\n"240 out += "- Goal: " + strconv.FormatUint(c.Goal, 10) + "\n"241 out += "- Pledged: " + strconv.FormatUint(c.Pledged, 10) + "\n"242 out += "- Deadline: block " + strconv.FormatInt(c.Deadline, 10) + "\n"243 out += "- Creator: `" + c.Creator.String() + "`\n\n"244 return false245 })246 return out247}248Claim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Launch(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, goal uint64, durationBlocks int) int
Pledge(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, amount uint64)
Refund(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Render(path string) string
Unpledge(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, amount uint64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.