← Back to Realms

Realm

gno.land/r/moul/x/daily/crowdfund/v1

Overview

Realm Path
gno.land/r/moul/x/daily/crowdfund/v1
Exported Functions
6
State Entries
2
Source Files
4
Total Package Entries
12

Exported Functions

6 exported functions

State

2 state entries

Source Code

FILES
crowdfund.gno
go
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/moul/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}
232

Raw Package Data

Raw JSON data