Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

crowdfund.gno

6.20 Kb · 231 lines
  1// Package crowdfund is an accounting-only port of the classic Solidity
  2// 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 the
  6// 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, exactly
  9// like the mapping(address => uint) balances of the Solidity original.
 10package crowdfund
 11
 12import (
 13	"strconv"
 14
 15	"chain"
 16	"chain/runtime"
 17	"chain/runtime/unsafe"
 18
 19	"gno.land/p/nym-moultest123/kit/store/v0"
 20	"gno.land/p/nt/avl/v0"
 21)
 22
 23// Campaign holds the state of a single crowdfunding campaign. It carries no ID
 24// field: the id belongs to the store, which hands it back on lookup and
 25// iteration.
 26type Campaign struct {
 27	Creator  address
 28	Goal     uint64
 29	Deadline int64     // block height after which the campaign is closed
 30	Pledged  uint64    // running total pledged
 31	Claimed  bool      // creator already claimed the funds
 32	pledges  *avl.Tree // backer address (string) -> pledged uint64
 33}
 34
 35// campaigns assigns the campaign ids. v0 kept its own nextID plus a key() that
 36// zero-padded to width 12, which stopped ordering Render past 10^12, and its
 37// own mustGet whose panic did not say which campaign was missing.
 38var campaigns = store.Named("campaign")
 39
 40func mustGet(id int) *Campaign {
 41	return campaigns.MustGet(store.ID(id)).(*Campaign)
 42}
 43
 44// Launch creates a new campaign owned by the caller and returns its id.
 45func Launch(cur realm, goal uint64, durationBlocks int) int {
 46	if goal == 0 {
 47		panic("goal must be > 0")
 48	}
 49	if durationBlocks <= 0 {
 50		panic("duration must be > 0")
 51	}
 52	caller := unsafe.PreviousRealm().Address()
 53	id := campaigns.Add(&Campaign{
 54		Creator:  caller,
 55		Goal:     goal,
 56		Deadline: runtime.ChainHeight() + int64(durationBlocks),
 57		pledges:  avl.NewTree(),
 58	})
 59	chain.Emit("Launch",
 60		"id", id.String(),
 61		"creator", caller.String(),
 62		"goal", strconv.FormatUint(goal, 10))
 63	return int(id)
 64}
 65
 66// Pledge backs campaign id with amount (before the deadline).
 67func Pledge(cur realm, id int, amount uint64) {
 68	if amount == 0 {
 69		panic("amount must be > 0")
 70	}
 71	c := mustGet(id)
 72	if runtime.ChainHeight() > c.Deadline {
 73		panic("campaign ended")
 74	}
 75	caller := unsafe.PreviousRealm().Address()
 76	prev := uint64(0)
 77	if v := c.pledges.Get(caller.String()); v != nil {
 78		prev = v.(uint64)
 79	}
 80	c.pledges.Set(caller.String(), prev+amount)
 81	c.Pledged += amount
 82	chain.Emit("Pledge",
 83		"id", strconv.Itoa(id),
 84		"from", caller.String(),
 85		"amount", strconv.FormatUint(amount, 10))
 86}
 87
 88// Unpledge withdraws amount from the caller's pledge (before the deadline).
 89func Unpledge(cur realm, id int, amount uint64) {
 90	if amount == 0 {
 91		panic("amount must be > 0")
 92	}
 93	c := mustGet(id)
 94	if runtime.ChainHeight() > c.Deadline {
 95		panic("campaign ended")
 96	}
 97	caller := unsafe.PreviousRealm().Address()
 98	v := c.pledges.Get(caller.String())
 99	if v == nil {
100		panic("nothing pledged")
101	}
102	have := v.(uint64)
103	if amount > have {
104		panic("amount exceeds pledge")
105	}
106	if remaining := have - amount; remaining == 0 {
107		c.pledges.Remove(caller.String())
108	} else {
109		c.pledges.Set(caller.String(), remaining)
110	}
111	c.Pledged -= amount
112	chain.Emit("Unpledge",
113		"id", strconv.Itoa(id),
114		"from", caller.String(),
115		"amount", strconv.FormatUint(amount, 10))
116}
117
118// Claim lets the creator take the funds if the goal was met after the deadline.
119func Claim(cur realm, id int) {
120	c := mustGet(id)
121	if runtime.ChainHeight() <= c.Deadline {
122		panic("campaign not ended")
123	}
124	caller := unsafe.PreviousRealm().Address()
125	if caller != c.Creator {
126		panic("only creator can claim")
127	}
128	if c.Pledged < c.Goal {
129		panic("goal not reached")
130	}
131	if c.Claimed {
132		panic("already claimed")
133	}
134	c.Claimed = true
135	chain.Emit("Claim",
136		"id", strconv.Itoa(id),
137		"amount", strconv.FormatUint(c.Pledged, 10))
138}
139
140// Refund returns the caller's pledge if the campaign failed after the deadline.
141func Refund(cur realm, id int) {
142	c := mustGet(id)
143	if runtime.ChainHeight() <= c.Deadline {
144		panic("campaign not ended")
145	}
146	if c.Pledged >= c.Goal {
147		panic("campaign succeeded, no refund")
148	}
149	caller := unsafe.PreviousRealm().Address()
150	v := c.pledges.Get(caller.String())
151	if v == nil {
152		panic("nothing to refund")
153	}
154	amount := v.(uint64)
155	c.pledges.Remove(caller.String())
156	c.Pledged -= amount
157	chain.Emit("Refund",
158		"id", strconv.Itoa(id),
159		"to", caller.String(),
160		"amount", strconv.FormatUint(amount, 10))
161}
162
163// --- pure/read-only helpers (also used by Render) ---
164
165// pct returns the funded percentage (0..100).
166func pct(pledged, goal uint64) int {
167	if goal == 0 {
168		return 0
169	}
170	p := int(pledged * 100 / goal)
171	if p > 100 {
172		p = 100
173	}
174	return p
175}
176
177// progressBar renders a fixed-width ▓░ bar for pledged/goal.
178func progressBar(pledged, goal uint64) string {
179	const width = 20
180	filled := 0
181	if goal > 0 {
182		filled = int(pledged * width / goal)
183		if filled > width {
184			filled = width
185		}
186	}
187	bar := ""
188	for i := 0; i < width; i++ {
189		if i < filled {
190			bar += "▓"
191		} else {
192			bar += "░"
193		}
194	}
195	return bar
196}
197
198// state describes a campaign relative to a given block height.
199func state(c *Campaign, height int64) string {
200	if height <= c.Deadline {
201		return "Active"
202	}
203	if c.Pledged >= c.Goal {
204		if c.Claimed {
205			return "Funded (claimed)"
206		}
207		return "Funded"
208	}
209	return "Failed"
210}
211
212// Render shows all campaigns with a progress bar, goal, pledged and state.
213func Render(path string) string {
214	if campaigns.Len() == 0 {
215		return "# Crowdfund\n\nNo campaigns yet. Call `Launch(goal, durationBlocks)` to start one.\n"
216	}
217	height := runtime.ChainHeight()
218	out := "# Crowdfund\n\n"
219	out += "Current block height: **" + strconv.FormatInt(height, 10) + "**\n\n"
220	campaigns.Each(func(id store.ID, v any) {
221		c := v.(*Campaign)
222		out += "## Campaign #" + id.String() + "\n\n"
223		out += "`" + progressBar(c.Pledged, c.Goal) + "` " + strconv.Itoa(pct(c.Pledged, c.Goal)) + "%\n\n"
224		out += "- State: **" + state(c, height) + "**\n"
225		out += "- Goal: " + strconv.FormatUint(c.Goal, 10) + "\n"
226		out += "- Pledged: " + strconv.FormatUint(c.Pledged, 10) + "\n"
227		out += "- Deadline: block " + strconv.FormatInt(c.Deadline, 10) + "\n"
228		out += "- Creator: `" + c.Creator.String() + "`\n\n"
229	})
230	return out
231}