1// Package bond is the create-bond policy for gnomemepad — separate from pad
2// so promo schedules can change without redeploying the pad realm.
3//
4// Pad calls CurrentBondUgnot() at Create time. This package owns:
5// - normal bond (default 2 GNOT)
6// - promo bond (default ~$20 in ugnot at a fixed GNOT rate, admin-adjustable)
7// - 10-day window OR admin EndPromo → back to normal
8//
9// USD is not on-chain: promo amount is ugnot. Admin sets PromoBondUgnot to
10// approximate $20 (e.g. at $1/GNOT → 20e6 ugnot; at $0.5/GNOT → 40e6).
11package bond
12
13import (
14 "chain"
15 "strconv"
16 "time"
17)
18
19const (
20 DenomUgnot = "ugnot"
21
22 // Defaults (ugnot, 1 GNOT = 1e6 ugnot)
23 // Pearl / current policy: 100 GNOT create bond (admin can SetNormalBond).
24 DefaultNormalBondUgnot int64 = 100_000_000 // 100 GNOT
25 DefaultPromoBondUgnot int64 = 100_000_000 // same unless StartPromo changes it
26 DefaultPromoDuration int64 = 10 * 24 * 60 * 60 // 10 days (seconds)
27
28 // Status for UI
29 StatusNormal = 0
30 StatusPromo = 1
31)
32
33var (
34 inited bool
35 admin address
36
37 normalBondUgnot int64
38 promoBondUgnot int64
39 // promo: active while time.Now().Unix() < promoEndsAt and !promoEnded
40 promoEndsAt int64
41 promoEnded bool // admin force-end (or auto after window)
42 promoStartedAt int64
43)
44
45// Init sets admin (deploy EOA) and default bond amounts. Call once.
46func Init(cur realm) {
47 if inited {
48 panic("bond: already initialized")
49 }
50 if !cur.Previous().IsUserCall() {
51 panic("bond: EOA only")
52 }
53 admin = cur.Previous().Address()
54 normalBondUgnot = DefaultNormalBondUgnot
55 promoBondUgnot = DefaultPromoBondUgnot
56 promoEndsAt = 0
57 promoEnded = true
58 promoStartedAt = 0
59 inited = true
60 chain.Emit("Init", "admin", admin.String())
61}
62
63func requireInit() {
64 if !inited {
65 panic("bond: call Init first")
66 }
67}
68
69func requireAdmin(cur realm) {
70 requireInit()
71 if !cur.Previous().IsUserCall() {
72 panic("bond: EOA only")
73 }
74 if cur.Previous().Address() != admin {
75 panic("bond: not admin")
76 }
77}
78
79// Admin returns the admin (deploy) address.
80func Admin() string {
81 requireInit()
82 return admin.String()
83}
84
85// TransferAdmin rotates admin (current admin only).
86func TransferAdmin(cur realm, newAdmin address) {
87 requireAdmin(cur)
88 if !newAdmin.IsValid() {
89 panic("bond: invalid address")
90 }
91 old := admin
92 admin = newAdmin
93 chain.Emit("TransferAdmin", "from", old.String(), "to", newAdmin.String())
94}
95
96// IsPromoActive reports whether the promo window is currently in force.
97func IsPromoActive() bool {
98 if !inited || promoEnded || promoEndsAt <= 0 {
99 return false
100 }
101 now := time.Now().Unix()
102 if now >= promoEndsAt {
103 return false
104 }
105 return true
106}
107
108// CurrentBondUgnot is the create bond pad must require (cross-safe read).
109// Pad: if sent < CurrentBondUgnot() → panic underpaid.
110func CurrentBondUgnot() int64 {
111 requireInit()
112 if IsPromoActive() {
113 return promoBondUgnot
114 }
115 return normalBondUgnot
116}
117
118// NormalBondUgnot returns the post-promo / default bond.
119func NormalBondUgnot() int64 {
120 requireInit()
121 return normalBondUgnot
122}
123
124// PromoBondUgnot returns the promo-period bond amount (may be inactive).
125func PromoBondUgnot() int64 {
126 requireInit()
127 return promoBondUgnot
128}
129
130// PromoEndsAt returns unix end of promo (0 if never started).
131func PromoEndsAt() int64 {
132 requireInit()
133 return promoEndsAt
134}
135
136// PromoSecondsLeft returns remaining promo seconds (0 if inactive).
137func PromoSecondsLeft() int64 {
138 if !IsPromoActive() {
139 return 0
140 }
141 left := promoEndsAt - time.Now().Unix()
142 if left < 0 {
143 return 0
144 }
145 return left
146}
147
148// Status: 0=normal 1=promo active.
149func Status() int {
150 if IsPromoActive() {
151 return StatusPromo
152 }
153 return StatusNormal
154}
155
156// BondInfo: status|currentUgnot|normalUgnot|promoUgnot|endsAt|secondsLeft|admin
157func BondInfo() string {
158 requireInit()
159 st := Status()
160 return strconv.Itoa(st) + "|" +
161 strconv.FormatInt(CurrentBondUgnot(), 10) + "|" +
162 strconv.FormatInt(normalBondUgnot, 10) + "|" +
163 strconv.FormatInt(promoBondUgnot, 10) + "|" +
164 strconv.FormatInt(promoEndsAt, 10) + "|" +
165 strconv.FormatInt(PromoSecondsLeft(), 10) + "|" +
166 admin.String()
167}
168
169// StartPromo begins a promo window from now.
170// promoUgnot: bond during promo (e.g. 20e6 ≈ $20 at $1/GNOT). 0 → keep current promoBondUgnot.
171// durationSec: window length. 0 → DefaultPromoDuration (10 days).
172func StartPromo(cur realm, promoUgnot, durationSec int64) {
173 requireAdmin(cur)
174 if promoUgnot < 0 {
175 panic("bond: promoUgnot must be non-negative")
176 }
177 if promoUgnot > 0 {
178 promoBondUgnot = promoUgnot
179 }
180 if promoBondUgnot <= 0 {
181 panic("bond: set promo bond amount first")
182 }
183 if durationSec <= 0 {
184 durationSec = DefaultPromoDuration
185 }
186 now := time.Now().Unix()
187 promoStartedAt = now
188 promoEndsAt = now + durationSec
189 promoEnded = false
190 chain.Emit("StartPromo",
191 "promoUgnot", strconv.FormatInt(promoBondUgnot, 10),
192 "endsAt", strconv.FormatInt(promoEndsAt, 10),
193 "durationSec", strconv.FormatInt(durationSec, 10),
194 )
195}
196
197// EndPromo forces normal bond immediately (deploy admin).
198func EndPromo(cur realm) {
199 requireAdmin(cur)
200 promoEnded = true
201 // keep promoEndsAt for history
202 chain.Emit("EndPromo",
203 "at", strconv.FormatInt(time.Now().Unix(), 10),
204 "normalUgnot", strconv.FormatInt(normalBondUgnot, 10),
205 )
206}
207
208// SetNormalBond updates the post-promo bond (default 2 GNOT).
209func SetNormalBond(cur realm, ugnot int64) {
210 requireAdmin(cur)
211 if ugnot <= 0 {
212 panic("bond: normal bond must be positive")
213 }
214 normalBondUgnot = ugnot
215 chain.Emit("SetNormalBond", "ugnot", strconv.FormatInt(ugnot, 10))
216}
217
218// SetPromoBond updates promo amount without restarting the clock.
219// Use to re-target ~$20 when GNOT price moves during the window.
220func SetPromoBond(cur realm, ugnot int64) {
221 requireAdmin(cur)
222 if ugnot <= 0 {
223 panic("bond: promo bond must be positive")
224 }
225 promoBondUgnot = ugnot
226 chain.Emit("SetPromoBond", "ugnot", strconv.FormatInt(ugnot, 10))
227}
228
229// ExtendPromo adds seconds to the current end (or starts from now if inactive).
230func ExtendPromo(cur realm, extraSec int64) {
231 requireAdmin(cur)
232 if extraSec <= 0 {
233 panic("bond: extraSec must be positive")
234 }
235 now := time.Now().Unix()
236 if !IsPromoActive() {
237 // restart promo with current promoBondUgnot
238 promoStartedAt = now
239 promoEndsAt = now + extraSec
240 promoEnded = false
241 } else {
242 promoEndsAt += extraSec
243 promoEnded = false
244 }
245 chain.Emit("ExtendPromo",
246 "endsAt", strconv.FormatInt(promoEndsAt, 10),
247 "extraSec", strconv.FormatInt(extraSec, 10),
248 )
249}
250Admin() string
BondInfo() string
CurrentBondUgnot() int64
EndPromo(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})
ExtendPromo(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}, extraSec int64)
Init(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})
IsPromoActive() bool
NormalBondUgnot() int64
PromoBondUgnot() int64
PromoEndsAt() int64
PromoSecondsLeft() int64
Render(path string) string
SetNormalBond(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}, ugnot int64)
SetPromoBond(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}, ugnot int64)
StartPromo(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}, promoUgnot int64, durationSec int64)
Status() int
TransferAdmin(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}, newAdmin string)
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.