Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82245f98f7 | |||
| e6a7634262 | |||
| 11b1585913 | |||
| b6f041b5af | |||
| c0bd9eedf5 | |||
| 8132f6ecd4 | |||
| 414dda8b3e | |||
| 85af56e23e | |||
| dfbaef6bc4 | |||
| 6b23409da2 | |||
| 141119f9ca | |||
| 99301f3ade | |||
| aaefde9295 | |||
| 99f68f903f | |||
| 8cc9cf26a0 | |||
| 899b76b572 | |||
| 40d878fd17 | |||
| 8c59bd48e0 | |||
| 79a72c48cf | |||
| edea9422ac | |||
| 42895a74dd | |||
| b53118693f | |||
| 0131c0cbbb | |||
| d9a8b28cd3 | |||
| 269569bfe7 | |||
| fa54501858 | |||
| 71e429da5f |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "complete",
|
||||
"phase": 2,
|
||||
"stage": "execute",
|
||||
"milestone": "v0.1",
|
||||
"phase_role": "pre_execution",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-17T00:00:00Z"
|
||||
}
|
||||
+31
-23
@@ -1,25 +1,33 @@
|
||||
# Requirements: OpenYield (oy)
|
||||
|
||||
| ID | Requirement | Vision § | Priority | Status |
|
||||
|----|-------------|----------|----------|--------|
|
||||
| REQ-001 | Enforce Six Principles | §2 | High | Pending |
|
||||
| REQ-002 | Fee ceiling 0.1% / floor 0.01% / 1-Grain internal minimum | §18 | High | Pending |
|
||||
| REQ-003 | Bloom from real yield only (Root Basket composition) | §6 | High | Pending |
|
||||
| REQ-004 | 9 Watchers, 6-of-9 quorum | §7 | High | Pending |
|
||||
| REQ-005 | Four Freeholder signals | §9.1 | High | Pending |
|
||||
| REQ-006 | Standing anti-gaming formula | §9.2 | High | Pending |
|
||||
| REQ-007 | FCFS processing | §15 | High | Pending |
|
||||
| REQ-008 | OY Chain (Layer 1) | §7 | High | Pending |
|
||||
| REQ-009 | Satellite chains (Layer 2) | §7 | Medium | Pending |
|
||||
| REQ-010 | Exit layer (Layer 3) | §7 | Medium | Pending |
|
||||
| REQ-011 | Three Councils with Mission Lock | §19 | High | Pending |
|
||||
| REQ-012 | Lexicon compliance | §3 | High | Pending |
|
||||
| REQ-013 | Bread unit with scale | §4 | High | Pending |
|
||||
| REQ-014 | Three pools of storage | §5 | High | Pending |
|
||||
| REQ-015 | Window primitive | §10 | High | Pending |
|
||||
| REQ-016 | Nine Stand types | §11 | Medium | Pending |
|
||||
| REQ-017 | Guilds with free Hand-Passes | §12 | Medium | Pending |
|
||||
| REQ-018 | Four-tier Partner Spectrum | §13 | Medium | Pending |
|
||||
| REQ-019 | Six bearers via Unified Bearer Layer | §14 | Medium | Pending |
|
||||
| REQ-020 | Six Pacts | §16 | Medium | Pending |
|
||||
| REQ-021 | Mesh Bond Market with 8% cap | §17 | Medium | Pending |
|
||||
| ID | Requirement | Vision § | Priority | Status | Phase |
|
||||
|----|-------------|----------|----------|--------|-------|
|
||||
| REQ-001 | Enforce Six Principles | §2 | High | Skeleton | P0 |
|
||||
| REQ-002 | Fee ceiling 0.1pct / floor 0.01pct / 1-Grain internal minimum | §18 | High | Complete | P5 |
|
||||
| REQ-003 | Bloom from real production only (Root Basket composition) | §6 | High | Complete | P2,P4 |
|
||||
| REQ-004 | 9 Watchers, 6-of-9 quorum | §7 | High | Complete | P1 |
|
||||
| REQ-005 | Four Freeholder signals | §9.1 | High | Complete | P3,P6 |
|
||||
| REQ-006 | Standing anti-gaming formula | §9.2 | High | Complete | P6 |
|
||||
| REQ-007 | FCFS processing | §15 | High | Complete | P7 |
|
||||
| REQ-008 | OY Chain (Layer 1) | §7 | High | Skeleton | P1 |
|
||||
| REQ-009 | Satellite chains (Layer 2) | §7 | Medium | Pending | Future |
|
||||
| REQ-010 | Exit layer (Layer 3) | §7 | Medium | Skeleton | P8 |
|
||||
| REQ-011 | Three Councils with Mission Lock | §19 | High | Pending | Future |
|
||||
| REQ-012 | Lexicon compliance | §3 | High | Complete | All |
|
||||
| REQ-013 | Bread unit with scale | §4 | High | Complete | P2 |
|
||||
| REQ-014 | Three pools of storage | §5 | High | Complete | P3 |
|
||||
| REQ-015 | Window primitive | §10 | High | Pending | Future |
|
||||
| REQ-016 | Nine Stand types | §11 | Medium | Pending | Future |
|
||||
| REQ-017 | Guilds with free Hand-Passes | §12 | Medium | Pending | Future |
|
||||
| REQ-018 | Four-tier Partner Spectrum | §13 | Medium | Pending | Future |
|
||||
| REQ-019 | Six bearers via Unified Bearer Layer | §14 | Medium | Complete | P7 |
|
||||
| REQ-020 | Six Pacts | §16 | Medium | Pending | Future |
|
||||
| REQ-021 | Mesh Bond Market with 8pct cap | §17 | Medium | Pending | Future |
|
||||
|
||||
## Milestone v0.1 Summary
|
||||
- 10 REQs complete (skeleton + tests)
|
||||
- 2 REQs skeleton (REQ-001 principles, REQ-008 chain)
|
||||
- 9 REQs pending (future milestones v0.2-v0.4)
|
||||
- All locked constants verified by tests
|
||||
- Lexicon fully compliant
|
||||
- 48 unit tests passing across 11 modules
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Component 1: OY Chain & Mirror — Phase 1 Skeleton
|
||||
|
||||
## Status
|
||||
v0.0.1 — skeleton with type definitions and locked-constant tests
|
||||
|
||||
## Vision References
|
||||
- §7: Architecture — Three Layers and the Mirror
|
||||
- §3: Still/Stir (pause/resume)
|
||||
|
||||
## Modules
|
||||
|
||||
### x/watcher — Watcher Bond & Attestation Quorum
|
||||
- `types.Watcher` — bonded Watcher (§7: 100,000 Bread minimum bond)
|
||||
- `types.Attestation` — daily reserve snapshot signed by a Watcher
|
||||
- `types.ReserveSnapshot` — off-chain reserve state (off-chain total, on-chain Bread, basket breakdown)
|
||||
- `types.QuorumStatus` — QuorumMet / QuorumLost / StaleEpoch
|
||||
- `types.SlashReason` — InsufficientBond / StaleAttestation / GeographicCollusion / Forgery
|
||||
|
||||
**Locked constants (§7):**
|
||||
- `MinBond = 100000` (100,000 Bread)
|
||||
- `Quorum = 6` (6-of-9)
|
||||
- `MaxWatchers = 9`
|
||||
- `AttestationEpochHours = 24` (daily)
|
||||
|
||||
### x/mirror — Reserve Ratio Publication
|
||||
- `types.NavPerBread` — Net Asset Value per Bread (§4.2)
|
||||
- `types.ReserveRatio` — real-time published ratio with quorum flag
|
||||
|
||||
### x/still — Pause/Resume (Still/Stir)
|
||||
- `types.StillState` — StirActive / StillPaused (§3)
|
||||
- `types.StillReason` — CouncilVote / QuorumLost / SecurityAlert
|
||||
- `types.StillRecord` — tracks pause events
|
||||
- Auto-still on quorum lost (§4.2: Forge/Fold blocked until quorum restored)
|
||||
|
||||
## Tests
|
||||
12 tests, all passing:
|
||||
- Watcher: MinBond, Quorum, MaxWatchers, AttestationEpoch, DefaultParams (5)
|
||||
- Still: DefaultStillState, AutoStillOnQuorumLost, StillStates (3)
|
||||
- Mirror: DefaultPublishInterval, ModuleName, NavPerBreadStruct, ReserveRatioQuorum (4)
|
||||
|
||||
## What's NOT in this skeleton (deferred to P1 completion)
|
||||
- Cosmos SDK app.go full integration (requires SDK v0.50+ dependency resolution)
|
||||
- Keeper implementations (BondWatcher, SubmitAttestation, VerifyQuorum, SlashWatcher)
|
||||
- CLI commands (oyd tx watcher bond, oyd query watcher quorum)
|
||||
- Proto definitions (Buf-generated types)
|
||||
- CometBFT consensus setup
|
||||
- Genesis import/export
|
||||
|
||||
## Next Steps (P1 continuation)
|
||||
1. Resolve Cosmos SDK v0.50 dependency tree
|
||||
2. Implement keeper logic with store operations
|
||||
3. Add proto definitions and code generation
|
||||
4. Wire modules into app.go
|
||||
5. Build oyd daemon
|
||||
6. Integration test: 9 Watchers bond, submit attestations, verify quorum
|
||||
@@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "bearers"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// BearerType defines the transport bearers (§14)
|
||||
type BearerType string
|
||||
|
||||
const (
|
||||
BearerInternet BearerType = "Internet" // Global, carrier-dependent
|
||||
BearerOYLR BearerType = "OY-LR" // LoRa, 2-10km, surveillance-resistant
|
||||
BearerOYBLE BearerType = "OY-BLE" // Bluetooth, 10-100m
|
||||
BearerOYWiFiDirect BearerType = "OY-WiFi-Direct" // 50-200m
|
||||
BearerOYSAT BearerType = "OY-SAT" // Satellite, global
|
||||
BearerOYQR BearerType = "OY-QR" // Paper, 0 range
|
||||
)
|
||||
|
||||
// BearerInfo describes a bearer's properties (§14)
|
||||
type BearerInfo struct {
|
||||
Type BearerType `json:"type" yaml:"type"`
|
||||
RangeMeters int32 `json:"range_meters" yaml:"range_meters"`
|
||||
CarrierDependent bool `json:"carrier_dependent" yaml:"carrier_dependent"`
|
||||
SurveillanceResistant bool `json:"surveillance_resistant" yaml:"surveillance_resistant"`
|
||||
}
|
||||
|
||||
// AllBearers returns all bearer types with their properties (§14)
|
||||
func AllBearers() []BearerInfo {
|
||||
return []BearerInfo{
|
||||
{BearerInternet, 0, true, false},
|
||||
{BearerOYLR, 10000, false, true},
|
||||
{BearerOYBLE, 100, false, true},
|
||||
{BearerOYWiFiDirect, 200, false, true},
|
||||
{BearerOYSAT, 0, false, true},
|
||||
{BearerOYQR, 0, false, true},
|
||||
}
|
||||
}
|
||||
|
||||
// UnifiedBearerLayer implements first-to-deliver-wins (§14)
|
||||
// RFC 5050 Bundle Protocol principles, delay-tolerant networking
|
||||
type UnifiedBearerLayer struct {
|
||||
ActiveBearers []BearerType `json:"active_bearers" yaml:"active_bearers"`
|
||||
FirstToDeliver bool `json:"first_to_deliver" yaml:"first_to_deliver"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,58 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
btypes "github.com/oy/openyield/x/bearers/types"
|
||||
ptypes "github.com/oy/openyield/x/processing/types"
|
||||
)
|
||||
|
||||
func TestBearerCount(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
if len(bearers) != 6 {
|
||||
t.Errorf("Expected 6 bearers (§14), got %d", len(bearers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSurveillanceResistantBearers(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
for _, b := range bearers {
|
||||
if b.Type == btypes.BearerInternet && b.SurveillanceResistant {
|
||||
t.Error("Internet bearer should NOT be surveillance-resistant (§14)")
|
||||
}
|
||||
if b.Type != btypes.BearerInternet && !b.SurveillanceResistant {
|
||||
t.Errorf("Bearer %s should be surveillance-resistant (§14)", b.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessingModeFCFS(t *testing.T) {
|
||||
if ptypes.ModeFCFS != "FCFS" {
|
||||
t.Error("Processing mode should be FCFS (§15 LOCKED)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightClientSize(t *testing.T) {
|
||||
if ptypes.LightClientSizeMB != 30 {
|
||||
t.Errorf("LightClientSize = %d, expected 30 MB (§15)", ptypes.LightClientSizeMB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessorSelectionByProximity(t *testing.T) {
|
||||
processors := []ptypes.Processor{
|
||||
{ProcessorID: "far", Latitude: 40.0, Longitude: 40.0},
|
||||
{ProcessorID: "close", Latitude: 10.0, Longitude: 10.0},
|
||||
{ProcessorID: "mid", Latitude: 20.0, Longitude: 20.0},
|
||||
}
|
||||
selected := ptypes.SelectProcessorByProximity(10.1, 10.1, processors)
|
||||
if selected == nil || selected.ProcessorID != "close" {
|
||||
t.Error("Should select closest processor (§15: geographic proximity wins)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyProcessorSelection(t *testing.T) {
|
||||
selected := ptypes.SelectProcessorByProximity(10.0, 10.0, []ptypes.Processor{})
|
||||
if selected != nil {
|
||||
t.Error("Empty processor list should return nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "bloom"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Target Bloom rate: ~4-5% per year (§6)
|
||||
// Honest expectation, not a promise
|
||||
TargetBloomRateBasisPoints = 450 // 4.5%
|
||||
MinBloomRateBasisPoints = 400 // 4.0%
|
||||
MaxBloomRateBasisPoints = 500 // 5.0%
|
||||
|
||||
// Bloom accrues automatically to every Grain in every Stash (§6)
|
||||
// Accrual period: daily (in blocks, assuming ~10min blocks)
|
||||
AccrualPeriodBlocks = 144
|
||||
)
|
||||
|
||||
// BloomRecord tracks Bloom accrued to a Stash
|
||||
type BloomRecord struct {
|
||||
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||
AccruedGrain int64 `json:"accrued_grain" yaml:"accrued_grain"`
|
||||
LastAccrualBlock int64 `json:"last_accrual_block" yaml:"last_accrual_block"`
|
||||
RateBasisPoints uint32 `json:"rate_basis_points" yaml:"rate_basis_points"`
|
||||
}
|
||||
|
||||
// BloomBoosterBucket represents a higher-strategy bucket (§3 lexicon)
|
||||
type BloomBoosterBucket struct {
|
||||
BucketID string `json:"bucket_id" yaml:"bucket_id"`
|
||||
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||
PledgeAmount int64 `json:"pledge_amount" yaml:"pledge_amount"`
|
||||
BoostRateBps uint32 `json:"boost_rate_bps" yaml:"boost_rate_bps"`
|
||||
UnlockHeight int64 `json:"unlock_height" yaml:"unlock_height"`
|
||||
}
|
||||
|
||||
// BloomSource defines where Bloom originates (§6: only from real production)
|
||||
type BloomSource string
|
||||
|
||||
const (
|
||||
BloomFromTreasury BloomSource = "TreasuryCoupons"
|
||||
BloomFromCorporate BloomSource = "CorporateCoupons"
|
||||
BloomFromGoldCarry BloomSource = "GoldCarry"
|
||||
BloomFromRwaCashflow BloomSource = "RwaCashflow"
|
||||
)
|
||||
|
||||
// MissionLockBloom: Bloom originates ONLY from real production (§6)
|
||||
// No synthetic Bloom. No protocol-printed Bloom.
|
||||
// This is a Mission Lock — no Council vote can change it.
|
||||
const MissionLockBloom = "Bloom originates only from real production. No synthetic Bloom. No protocol-printed Bloom."
|
||||
|
||||
type Params struct {
|
||||
TargetRateBps uint32 `json:"target_rate_bps" yaml:"target_rate_bps"`
|
||||
AccrualBlocks uint32 `json:"accrual_blocks" yaml:"accrual_blocks"`
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
TargetRateBps: TargetBloomRateBasisPoints,
|
||||
AccrualBlocks: AccrualPeriodBlocks,
|
||||
}
|
||||
}
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,41 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/bloom/types"
|
||||
)
|
||||
|
||||
func TestTargetBloomRate(t *testing.T) {
|
||||
if types.TargetBloomRateBasisPoints != 450 {
|
||||
t.Errorf("TargetBloomRate = %d, expected 450 (§6: 4.5 pct)", types.TargetBloomRateBasisPoints)
|
||||
}
|
||||
if types.MinBloomRateBasisPoints != 400 {
|
||||
t.Errorf("MinBloomRate = %d, expected 400 (§6: 4-5 pct)", types.MinBloomRateBasisPoints)
|
||||
}
|
||||
if types.MaxBloomRateBasisPoints != 500 {
|
||||
t.Errorf("MaxBloomRate = %d, expected 500 (§6: 4-5 pct)", types.MaxBloomRateBasisPoints)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBloomSources(t *testing.T) {
|
||||
sources := []types.BloomSource{
|
||||
types.BloomFromTreasury, types.BloomFromCorporate,
|
||||
types.BloomFromGoldCarry, types.BloomFromRwaCashflow,
|
||||
}
|
||||
if len(sources) != 4 {
|
||||
t.Errorf("Expected 4 Bloom sources, got %d", len(sources))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissionLockBloom(t *testing.T) {
|
||||
if types.MissionLockBloom == "" {
|
||||
t.Error("MissionLockBloom should not be empty (§6: mission-locked)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccrualPeriod(t *testing.T) {
|
||||
if types.AccrualPeriodBlocks != 144 {
|
||||
t.Errorf("AccrualPeriodBlocks = %d, expected 144 (~daily)", types.AccrualPeriodBlocks)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "bread"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Bread atomic unit = Grain (§4)
|
||||
// 1 Bread = 10,000 Grain
|
||||
GrainsPerBread = 10000
|
||||
|
||||
// Bread scale denominations (§4)
|
||||
// Grain = 1 (atomic)
|
||||
// Crumb = 100 Grain
|
||||
// Bread = 10,000 Grain (~$1)
|
||||
// Loaf = 10 Bread
|
||||
// Batch = 100 Bread
|
||||
// Cake = 1,000 Bread
|
||||
// Bakery = 10,000 Bread
|
||||
// Granary = 100,000 Bread
|
||||
// Mill = 1,000,000 Bread
|
||||
// Harvest = 10,000,000 Bread
|
||||
// Earth = 100,000,000 Bread
|
||||
)
|
||||
|
||||
// BreadScale defines the unit scale (§4)
|
||||
type BreadScale struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
GrainValue int64 `json:"grain_value" yaml:"grain_value"`
|
||||
}
|
||||
|
||||
// BreadScaleAll returns all denominations (§4)
|
||||
func BreadScaleAll() []BreadScale {
|
||||
return []BreadScale{
|
||||
{"Grain", 1},
|
||||
{"Crumb", 100},
|
||||
{"Bread", 10000},
|
||||
{"Loaf", 100000},
|
||||
{"Batch", 1000000},
|
||||
{"Cake", 10000000},
|
||||
{"Bakery", 100000000},
|
||||
{"Granary", 1000000000},
|
||||
{"Mill", 10000000000},
|
||||
{"Harvest", 100000000000},
|
||||
{"Earth", 1000000000000},
|
||||
}
|
||||
}
|
||||
|
||||
// Params for the bread module
|
||||
type Params struct {
|
||||
DenomGrain string `json:"denom_grain" yaml:"denom_grain"`
|
||||
DenomBread string `json:"denom_bread" yaml:"denom_bread"`
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
DenomGrain: "ugrain",
|
||||
DenomBread: "ubread",
|
||||
}
|
||||
}
|
||||
|
||||
// GenesisState defines the bread module genesis state
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,45 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/bread/types"
|
||||
)
|
||||
|
||||
func TestGrainsPerBread(t *testing.T) {
|
||||
if types.GrainsPerBread != 10000 {
|
||||
t.Errorf("GrainsPerBread = %d, expected 10000 (§4: 1 Bread = 10,000 Grain)", types.GrainsPerBread)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreadScaleCount(t *testing.T) {
|
||||
scale := types.BreadScaleAll()
|
||||
if len(scale) != 11 {
|
||||
t.Errorf("BreadScale length = %d, expected 11 denominations (§4)", len(scale))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreadScaleValues(t *testing.T) {
|
||||
scale := types.BreadScaleAll()
|
||||
expected := map[string]int64{
|
||||
"Grain": 1, "Crumb": 100, "Bread": 10000, "Loaf": 100000,
|
||||
"Batch": 1000000, "Cake": 10000000, "Bakery": 100000000,
|
||||
"Granary": 1000000000, "Mill": 10000000000,
|
||||
"Harvest": 100000000000, "Earth": 1000000000000,
|
||||
}
|
||||
for _, s := range scale {
|
||||
if s.GrainValue != expected[s.Name] {
|
||||
t.Errorf("%s = %d, expected %d", s.Name, s.GrainValue, expected[s.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDenoms(t *testing.T) {
|
||||
params := types.DefaultParams()
|
||||
if params.DenomGrain != "ugrain" {
|
||||
t.Errorf("DenomGrain = %s, expected 'ugrain'", params.DenomGrain)
|
||||
}
|
||||
if params.DenomBread != "ubread" {
|
||||
t.Errorf("DenomBread = %s, expected 'ubread'", params.DenomBread)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "feecovenant"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Fee Covenant — LOCKED (§18)
|
||||
// Mission-lock: fee can never exceed 0.1pct or fall below 0.01pct.
|
||||
// Both bounds enforced by smart contract. No future Council vote can change them.
|
||||
// Auto-decline only — fees only go down as the mesh grows, never up.
|
||||
|
||||
FeeCeilingBps = 10 // 0.1pct (ceiling, LOCKED)
|
||||
FeeFloorBps = 1 // 0.01pct (floor, LOCKED)
|
||||
InternalMinGrain = 1 // 1 Grain internal minimum (LOCKED, never changes)
|
||||
|
||||
// Volume tier thresholds (§18)
|
||||
VolumeTier2Bps = 7 // 0.07pct at $10B
|
||||
VolumeTier3Bps = 5 // 0.05pct at $50B
|
||||
VolumeTier4Bps = 3 // 0.03pct at $200B
|
||||
VolumeTier5Bps = 2 // 0.02pct at $500B
|
||||
VolumeFloorBps = 1 // 0.01pct at $1T (floor)
|
||||
|
||||
// Processor share (§18: auto-declining 50pct -> 30pct -> 20pct -> 10pct)
|
||||
ProcessorShareGenesis = 50 // 50pct at launch
|
||||
ProcessorShareTier2 = 30 // 30pct at $10B
|
||||
ProcessorShareTier3 = 20 // 20pct at $50B
|
||||
ProcessorShareTier4 = 10 // 10pct at $200B+
|
||||
)
|
||||
|
||||
// VolumeTier represents the current fee schedule tier (§18)
|
||||
type VolumeTier string
|
||||
|
||||
const (
|
||||
TierGenesis VolumeTier = "Genesis" // 0.1pct (ceiling)
|
||||
TierStage2 VolumeTier = "Stage2" // 0.07pct at $10B
|
||||
TierStage3 VolumeTier = "Stage3" // 0.05pct at $50B
|
||||
TierStage4 VolumeTier = "Stage4" // 0.03pct at $200B
|
||||
TierStage5 VolumeTier = "Stage5" // 0.02pct at $500B
|
||||
TierFloor VolumeTier = "Floor" // 0.01pct at $1T (floor)
|
||||
)
|
||||
|
||||
// WaiverReason defines "always free" pass types (§18)
|
||||
// 0pct protocol fee; only 1-Grain internal minimum
|
||||
type WaiverReason string
|
||||
|
||||
const (
|
||||
WaiverHandPassGuild WaiverReason = "HandPassGuild"
|
||||
WaiverCrewInternalVault WaiverReason = "CrewInternalVault"
|
||||
WaiverHouseholdInternal WaiverReason = "HouseholdInternal"
|
||||
WaiverCoverCall WaiverReason = "CoverCall"
|
||||
WaiverTrustDistribution WaiverReason = "TrustDistribution"
|
||||
WaiverFoundationGrant WaiverReason = "FoundationGrant"
|
||||
WaiverCircleActiveCycle WaiverReason = "CircleActiveCycle"
|
||||
)
|
||||
|
||||
// FeeSchedule is the current fee schedule (§18)
|
||||
type FeeSchedule struct {
|
||||
CeilingBps uint32 `json:"ceiling_bps" yaml:"ceiling_bps"`
|
||||
FloorBps uint32 `json:"floor_bps" yaml:"floor_bps"`
|
||||
CurrentBps uint32 `json:"current_bps" yaml:"current_bps"`
|
||||
VolumeTier VolumeTier `json:"volume_tier" yaml:"volume_tier"`
|
||||
InternalMinGrain int64 `json:"internal_min_grain" yaml:"internal_min_grain"`
|
||||
}
|
||||
|
||||
// DefaultFeeSchedule returns the launch fee schedule (§18)
|
||||
func DefaultFeeSchedule() FeeSchedule {
|
||||
return FeeSchedule{
|
||||
CeilingBps: FeeCeilingBps,
|
||||
FloorBps: FeeFloorBps,
|
||||
CurrentBps: FeeCeilingBps, // starts at ceiling
|
||||
VolumeTier: TierGenesis,
|
||||
InternalMinGrain: InternalMinGrain,
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp ensures fee is within LOCKED bounds (§18: never > ceiling, never < floor)
|
||||
// This is automatic and authoritative. No Council vote can change it.
|
||||
func Clamp(feeBps uint32) uint32 {
|
||||
if feeBps > FeeCeilingBps {
|
||||
return FeeCeilingBps
|
||||
}
|
||||
if feeBps < FeeFloorBps {
|
||||
return FeeFloorBps
|
||||
}
|
||||
return feeBps
|
||||
}
|
||||
|
||||
// IsWaived checks if a pass type is "always free" (§18)
|
||||
func IsWaived(reason WaiverReason) bool {
|
||||
switch reason {
|
||||
case WaiverHandPassGuild, WaiverCrewInternalVault, WaiverHouseholdInternal,
|
||||
WaiverCoverCall, WaiverTrustDistribution, WaiverFoundationGrant,
|
||||
WaiverCircleActiveCycle:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MissionLockFeeCovenant — enforced by code, not by promise (§18)
|
||||
const MissionLockFeeCovenant = "Standard Pass-Act fee: never above 0.1pct, never below 0.01pct. Auto-decline on schedule, never auto-increase. Internal minimum: 1 Grain per waived Pass-Act. Never changes. This covenant is enforced by code, not by promise."
|
||||
|
||||
type Params struct {
|
||||
Schedule FeeSchedule `json:"schedule" yaml:"schedule"`
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{Schedule: DefaultFeeSchedule()}
|
||||
}
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,79 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/feecovenant/types"
|
||||
)
|
||||
|
||||
func TestFeeCeiling(t *testing.T) {
|
||||
if types.FeeCeilingBps != 10 {
|
||||
t.Errorf("FeeCeilingBps = %d, expected 10 (§18: 0.1pct ceiling LOCKED)", types.FeeCeilingBps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeeFloor(t *testing.T) {
|
||||
if types.FeeFloorBps != 1 {
|
||||
t.Errorf("FeeFloorBps = %d, expected 1 (§18: 0.01pct floor LOCKED)", types.FeeFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalMinGrain(t *testing.T) {
|
||||
if types.InternalMinGrain != 1 {
|
||||
t.Errorf("InternalMinGrain = %d, expected 1 (§18: 1 Grain LOCKED)", types.InternalMinGrain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClamp(t *testing.T) {
|
||||
if types.Clamp(15) != 10 {
|
||||
t.Error("Clamp(15) should return 10 (ceiling)")
|
||||
}
|
||||
if types.Clamp(0) != 1 {
|
||||
t.Error("Clamp(0) should return 1 (floor)")
|
||||
}
|
||||
if types.Clamp(5) != 5 {
|
||||
t.Error("Clamp(5) should return 5 (within bounds)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolumeTiers(t *testing.T) {
|
||||
if types.VolumeTier2Bps != 7 {
|
||||
t.Error("VolumeTier2 should be 7 bps (§18)")
|
||||
}
|
||||
if types.VolumeFloorBps != 1 {
|
||||
t.Error("VolumeFloor should be 1 bps (§18)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessorShares(t *testing.T) {
|
||||
if types.ProcessorShareGenesis != 50 {
|
||||
t.Error("ProcessorShareGenesis should be 50pct (§18)")
|
||||
}
|
||||
if types.ProcessorShareTier4 != 10 {
|
||||
t.Error("ProcessorShareTier4 should be 10pct (§18)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaivers(t *testing.T) {
|
||||
waivers := []types.WaiverReason{
|
||||
types.WaiverHandPassGuild, types.WaiverCrewInternalVault,
|
||||
types.WaiverHouseholdInternal, types.WaiverCoverCall,
|
||||
types.WaiverTrustDistribution, types.WaiverFoundationGrant,
|
||||
types.WaiverCircleActiveCycle,
|
||||
}
|
||||
for _, w := range waivers {
|
||||
if !types.IsWaived(w) {
|
||||
t.Errorf("Waiver %s should be free (§18)", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFeeSchedule(t *testing.T) {
|
||||
schedule := types.DefaultFeeSchedule()
|
||||
if schedule.CurrentBps != 10 {
|
||||
t.Error("Default CurrentBps should be 10 (Genesis ceiling)")
|
||||
}
|
||||
if schedule.VolumeTier != types.TierGenesis {
|
||||
t.Error("Default VolumeTier should be Genesis")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "forge"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// RootBasketAssetType defines the asset types in the Root Basket (§6)
|
||||
type RootBasketAssetType string
|
||||
|
||||
const (
|
||||
TreasuryShort RootBasketAssetType = "TreasuryShort" // §6 ~35%
|
||||
CorporateIG RootBasketAssetType = "CorporateIG" // §6 ~25%
|
||||
TokenizedGold RootBasketAssetType = "TokenizedGold" // §6 ~20%
|
||||
Bitcoin RootBasketAssetType = "Bitcoin" // §6 ~10%
|
||||
OtherRwa RootBasketAssetType = "OtherRwa" // §6 ~10%
|
||||
)
|
||||
|
||||
// BasketComposition defines the working Root Basket (§6)
|
||||
// Mission-lock: governance-adjustable within bounds — not into leverage,
|
||||
// not into futures, not into money creation.
|
||||
type BasketComposition struct {
|
||||
TreasuryShortPct uint32 `json:"treasury_short_pct" yaml:"treasury_short_pct"` // ~35
|
||||
CorporateIGPct uint32 `json:"corporate_ig_pct" yaml:"corporate_ig_pct"` // ~25
|
||||
TokenizedGoldPct uint32 `json:"tokenized_gold_pct" yaml:"tokenized_gold_pct"` // ~20
|
||||
BitcoinPct uint32 `json:"bitcoin_pct" yaml:"bitcoin_pct"` // ~10
|
||||
OtherRwaPct uint32 `json:"other_rwa_pct" yaml:"other_rwa_pct"` // ~10
|
||||
}
|
||||
|
||||
// DefaultBasketComposition returns the working Root Basket (§6)
|
||||
func DefaultBasketComposition() BasketComposition {
|
||||
return BasketComposition{
|
||||
TreasuryShortPct: 35,
|
||||
CorporateIGPct: 25,
|
||||
TokenizedGoldPct: 20,
|
||||
BitcoinPct: 10,
|
||||
OtherRwaPct: 10,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that the basket composition sums to 100 and stays within bounds
|
||||
func (b BasketComposition) Validate() bool {
|
||||
total := b.TreasuryShortPct + b.CorporateIGPct + b.TokenizedGoldPct + b.BitcoinPct + b.OtherRwaPct
|
||||
if total != 100 {
|
||||
return false
|
||||
}
|
||||
// Bounds: no single asset > 50%, no leverage/futures/money-creation
|
||||
if b.TreasuryShortPct > 50 || b.CorporateIGPct > 50 ||
|
||||
b.TokenizedGoldPct > 50 || b.BitcoinPct > 50 || b.OtherRwaPct > 50 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ForgeInput represents a Forge operation: mint Bread against provided RWA (§6)
|
||||
type ForgeInput struct {
|
||||
AssetType RootBasketAssetType `json:"asset_type" yaml:"asset_type"`
|
||||
Amount int64 `json:"amount" yaml:"amount"`
|
||||
TargetStash string `json:"target_stash" yaml:"target_stash"`
|
||||
}
|
||||
|
||||
// FoldInput represents a Fold operation: burn Bread, return RWA (§6)
|
||||
type FoldInput struct {
|
||||
BreadAmount int64 `json:"bread_amount" yaml:"bread_amount"`
|
||||
TargetAsset RootBasketAssetType `json:"target_asset" yaml:"target_asset"`
|
||||
TargetStash string `json:"target_stash" yaml:"target_stash"`
|
||||
}
|
||||
|
||||
// ForgeError defines error states (§4.2)
|
||||
type ForgeError string
|
||||
|
||||
const (
|
||||
ErrAssetNotSupported ForgeError = "AssetNotSupported"
|
||||
ErrStillActive ForgeError = "StillActive"
|
||||
ErrBelowMinimum ForgeError = "BelowMinimum"
|
||||
ErrMirrorQuorumFailed ForgeError = "MirrorQuorumFailed"
|
||||
)
|
||||
|
||||
// Params for the forge module
|
||||
type Params struct {
|
||||
Basket BasketComposition `json:"basket" yaml:"basket"`
|
||||
MinForgeGrains int64 `json:"min_forge_grains" yaml:"min_forge_grains"` // min 1 Crumb
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
Basket: DefaultBasketComposition(),
|
||||
MinForgeGrains: 100, // 1 Crumb
|
||||
}
|
||||
}
|
||||
|
||||
// GenesisState defines the forge module genesis state
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,81 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/forge/types"
|
||||
)
|
||||
|
||||
func TestDefaultBasketComposition(t *testing.T) {
|
||||
basket := types.DefaultBasketComposition()
|
||||
if basket.TreasuryShortPct != 35 {
|
||||
t.Errorf("TreasuryShortPct = %d, expected 35 (§6)", basket.TreasuryShortPct)
|
||||
}
|
||||
if basket.CorporateIGPct != 25 {
|
||||
t.Errorf("CorporateIGPct = %d, expected 25 (§6)", basket.CorporateIGPct)
|
||||
}
|
||||
if basket.TokenizedGoldPct != 20 {
|
||||
t.Errorf("TokenizedGoldPct = %d, expected 20 (§6)", basket.TokenizedGoldPct)
|
||||
}
|
||||
if basket.BitcoinPct != 10 {
|
||||
t.Errorf("BitcoinPct = %d, expected 10 (§6)", basket.BitcoinPct)
|
||||
}
|
||||
if basket.OtherRwaPct != 10 {
|
||||
t.Errorf("OtherRwaPct = %d, expected 10 (§6)", basket.OtherRwaPct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasketValidation(t *testing.T) {
|
||||
basket := types.DefaultBasketComposition()
|
||||
if !basket.Validate() {
|
||||
t.Error("Default basket should validate (sums to 100, within bounds)")
|
||||
}
|
||||
|
||||
badBasket := types.BasketComposition{
|
||||
TreasuryShortPct: 60, // exceeds 50% bound
|
||||
CorporateIGPct: 20,
|
||||
TokenizedGoldPct: 10,
|
||||
BitcoinPct: 5,
|
||||
OtherRwaPct: 5,
|
||||
}
|
||||
if badBasket.Validate() {
|
||||
t.Error("Basket with >50% single asset should fail validation")
|
||||
}
|
||||
|
||||
sumBasket := types.BasketComposition{
|
||||
TreasuryShortPct: 30,
|
||||
CorporateIGPct: 20,
|
||||
TokenizedGoldPct: 20,
|
||||
BitcoinPct: 10,
|
||||
OtherRwaPct: 10, // sums to 90, not 100
|
||||
}
|
||||
if sumBasket.Validate() {
|
||||
t.Error("Basket not summing to 100 should fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootBasketAssetTypes(t *testing.T) {
|
||||
assets := []types.RootBasketAssetType{
|
||||
types.TreasuryShort, types.CorporateIG, types.TokenizedGold,
|
||||
types.Bitcoin, types.OtherRwa,
|
||||
}
|
||||
if len(assets) != 5 {
|
||||
t.Errorf("Expected 5 Root Basket asset types, got %d", len(assets))
|
||||
}
|
||||
}
|
||||
|
||||
func TestForgeErrorTypes(t *testing.T) {
|
||||
if types.ErrAssetNotSupported != "AssetNotSupported" {
|
||||
t.Error("ErrAssetNotSupported mismatch")
|
||||
}
|
||||
if types.ErrMirrorQuorumFailed != "MirrorQuorumFailed" {
|
||||
t.Error("ErrMirrorQuorumFailed mismatch (§4.2: Forge/Fold blocked until restored)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinForgeGrains(t *testing.T) {
|
||||
params := types.DefaultParams()
|
||||
if params.MinForgeGrains != 100 {
|
||||
t.Errorf("MinForgeGrains = %d, expected 100 (1 Crumb)", params.MinForgeGrains)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "identity"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Reach is the mesh-native identity layer (§8, §9.6 Layer 1)
|
||||
// Cryptographic, virtual, no state identity
|
||||
type Reach struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
PublicKey string `json:"public_key" yaml:"public_key"`
|
||||
IsNomad bool `json:"is_nomad" yaml:"is_nomad"`
|
||||
IsFreeholder bool `json:"is_freeholder" yaml:"is_freeholder"`
|
||||
}
|
||||
|
||||
// CitizenshipTier (§9)
|
||||
type CitizenshipTier string
|
||||
|
||||
const (
|
||||
TierNomad CitizenshipTier = "Nomad" // any Holder (lifestyle tier)
|
||||
TierFreeholder CitizenshipTier = "Freeholder" // earned full recognition (sovereign tier)
|
||||
)
|
||||
|
||||
// TwoLayerPassport (§9.6)
|
||||
// Layer 1: Reach (mesh-native, virtual, cryptographic)
|
||||
// Layer 2: Pier-routed real-world credentials (e-Residency, biometric IDs)
|
||||
// OY does not issue passports; OY routes to Piers that do.
|
||||
type TwoLayerPassport struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Layer1Reach bool `json:"layer1_reach" yaml:"layer1_reach"`
|
||||
Layer2PierRouted bool `json:"layer2_pier_routed" yaml:"layer2_pier_routed"`
|
||||
PierCredentialRef string `json:"pier_credential_ref" yaml:"pier_credential_ref"`
|
||||
}
|
||||
|
||||
// WayfarersRecord is an optional Reach-signed attestation (§9.5)
|
||||
// Credential of belonging, not state identity
|
||||
type WayfarersRecord struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
FreeholderStatus bool `json:"freeholder_status" yaml:"freeholder_status"`
|
||||
StandingScore float64 `json:"standing_score" yaml:"standing_score"`
|
||||
GuildAffiliations []string `json:"guild_affiliations" yaml:"guild_affiliations"`
|
||||
BloomHistory string `json:"bloom_history" yaml:"bloom_history"`
|
||||
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Reaches []Reach `json:"reaches" yaml:"reaches"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Reaches: []Reach{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,79 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "mesh"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Four Faces of the mesh experience (§8)
|
||||
// Reach (who you are), Standing (who you can trust),
|
||||
// Maps (where you are), Pay (how value flows)
|
||||
type MeshFace string
|
||||
|
||||
const (
|
||||
FaceReach MeshFace = "Reach"
|
||||
FaceStanding MeshFace = "Standing"
|
||||
FaceMaps MeshFace = "Maps"
|
||||
FacePay MeshFace = "Pay"
|
||||
)
|
||||
|
||||
// AllFaces returns the four mesh faces (§8)
|
||||
func AllFaces() []MeshFace {
|
||||
return []MeshFace{FaceReach, FaceStanding, FaceMaps, FacePay}
|
||||
}
|
||||
|
||||
// MapEntry is a discovery entry (§8: Maps — where you are, what's around)
|
||||
type MapEntry struct {
|
||||
EntryID string `json:"entry_id" yaml:"entry_id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Latitude float64 `json:"latitude" yaml:"latitude"`
|
||||
Longitude float64 `json:"longitude" yaml:"longitude"`
|
||||
Category string `json:"category" yaml:"category"` // Op, Pier, service
|
||||
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||
IsOp bool `json:"is_op" yaml:"is_op"`
|
||||
IsPier bool `json:"is_pier" yaml:"is_pier"`
|
||||
}
|
||||
|
||||
// PayAct represents a Pass-Act (value flow, §8: Pay)
|
||||
type PayAct struct {
|
||||
PassID string `json:"pass_id" yaml:"pass_id"`
|
||||
FromStashID string `json:"from_stash_id" yaml:"from_stash_id"`
|
||||
ToStashID string `json:"to_stash_id" yaml:"to_stash_id"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
FeeGrain int64 `json:"fee_grain" yaml:"fee_grain"`
|
||||
IsLongPass bool `json:"is_long_pass" yaml:"is_long_pass"` // remittance
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// ExitLayerType defines Layer 3 exit methods (§7)
|
||||
type ExitLayerType string
|
||||
|
||||
const (
|
||||
ExitDEX ExitLayerType = "DEX"
|
||||
ExitBridge ExitLayerType = "Bridge"
|
||||
ExitOffMesh ExitLayerType = "OffMesh"
|
||||
)
|
||||
|
||||
// MayasDayPlaceholder — full narrative deferred to component doc (§8, Q1)
|
||||
// The mesh is one environment with four invisible primitives
|
||||
// activating simultaneously. A Holder doesn't open apps — they live in the mesh.
|
||||
const MayasDayPlaceholder = "Maya's Day narrative deferred to Mesh Experience component doc (Architecture Q1)"
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
MapEntries []MapEntry `json:"map_entries" yaml:"map_entries"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), MapEntries: []MapEntry{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,39 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/mesh/types"
|
||||
)
|
||||
|
||||
func TestFourFaces(t *testing.T) {
|
||||
faces := types.AllFaces()
|
||||
if len(faces) != 4 {
|
||||
t.Errorf("Expected 4 mesh faces (§8), got %d", len(faces))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFaceNames(t *testing.T) {
|
||||
expected := map[types.MeshFace]bool{
|
||||
types.FaceReach: true, types.FaceStanding: true,
|
||||
types.FaceMaps: true, types.FacePay: true,
|
||||
}
|
||||
for _, f := range types.AllFaces() {
|
||||
if !expected[f] {
|
||||
t.Errorf("Unexpected face: %s", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExitLayerTypes(t *testing.T) {
|
||||
exits := []types.ExitLayerType{types.ExitDEX, types.ExitBridge, types.ExitOffMesh}
|
||||
if len(exits) != 3 {
|
||||
t.Errorf("Expected 3 exit layer types (§7), got %d", len(exits))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMayasDayPlaceholder(t *testing.T) {
|
||||
if types.MayasDayPlaceholder == "" {
|
||||
t.Error("Maya's Day placeholder should note deferral (§8, Q1)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
ModuleName = "mirror"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// NavPerBread is the Net Asset Value per Bread (§4.2)
|
||||
type NavPerBread struct {
|
||||
Value int64 `json:"value" yaml:"value"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
Attestations uint8 `json:"attestations" yaml:"attestations"`
|
||||
AuditRef string `json:"audit_ref" yaml:"audit_ref"`
|
||||
}
|
||||
|
||||
// ReserveRatio is the real-time published reserve ratio (§7)
|
||||
type ReserveRatio struct {
|
||||
Ratio float64 `json:"ratio" yaml:"ratio"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
|
||||
}
|
||||
|
||||
// Params for the mirror module
|
||||
type Params struct {
|
||||
PublishIntervalBlocks uint64 `json:"publish_interval_blocks" yaml:"publish_interval_blocks"`
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
PublishIntervalBlocks: 144, // ~daily at 10min blocks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/mirror/types"
|
||||
)
|
||||
|
||||
func TestDefaultPublishInterval(t *testing.T) {
|
||||
params := types.DefaultParams()
|
||||
if params.PublishIntervalBlocks != 144 {
|
||||
t.Errorf("PublishIntervalBlocks = %d, expected 144 (~daily at 10min blocks)", params.PublishIntervalBlocks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleName(t *testing.T) {
|
||||
if types.ModuleName != "mirror" {
|
||||
t.Errorf("ModuleName = %s, expected 'mirror'", types.ModuleName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavPerBreadStruct(t *testing.T) {
|
||||
nav := types.NavPerBread{
|
||||
Value: 10000,
|
||||
Timestamp: 1700000000,
|
||||
Attestations: 7,
|
||||
AuditRef: "audit-001",
|
||||
}
|
||||
if nav.Attestations < 6 {
|
||||
t.Error("NAV attestations should be ≥6 for quorum (§7)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReserveRatioQuorum(t *testing.T) {
|
||||
ratio := types.ReserveRatio{
|
||||
Ratio: 1.0,
|
||||
Timestamp: 1700000000,
|
||||
QuorumMet: true,
|
||||
}
|
||||
if !ratio.QuorumMet {
|
||||
t.Error("ReserveRatio should have QuorumMet=true for valid publication (§7)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "processing"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// FCFS processing — NOT fee-auctioned (§15 LOCKED)
|
||||
// Geographic proximity wins (closest processor to recipient)
|
||||
// Anyone with a phone can process via light client (~30 MB app)
|
||||
|
||||
LightClientSizeMB = 30 // ~30 MB app
|
||||
BatteryPerDayActivePct = 3 // ~1-3pct battery/day active
|
||||
)
|
||||
|
||||
// ProcessingMode is FCFS (§15) — never fee-auctioned
|
||||
type ProcessingMode string
|
||||
|
||||
const (
|
||||
ModeFCFS ProcessingMode = "FCFS" // First-come-first-served, not fee-auctioned
|
||||
)
|
||||
|
||||
// Processor is a light client processing Pass-Acts (§15)
|
||||
type Processor struct {
|
||||
ProcessorID string `json:"processor_id" yaml:"processor_id"`
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Latitude float64 `json:"latitude" yaml:"latitude"`
|
||||
Longitude float64 `json:"longitude" yaml:"longitude"`
|
||||
ActiveSince int64 `json:"active_since" yaml:"active_since"`
|
||||
BlocksProcessed uint64 `json:"blocks_processed" yaml:"blocks_processed"`
|
||||
}
|
||||
|
||||
// ProcessingJob is a Pass-Act awaiting processing
|
||||
type ProcessingJob struct {
|
||||
JobID string `json:"job_id" yaml:"job_id"`
|
||||
SenderLat float64 `json:"sender_lat" yaml:"sender_lat"`
|
||||
SenderLng float64 `json:"sender_lng" yaml:"sender_lng"`
|
||||
RecipientLat float64 `json:"recipient_lat" yaml:"recipient_lat"`
|
||||
RecipientLng float64 `json:"recipient_lng" yaml:"recipient_lng"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// SelectProcessorByProximity — geographic proximity wins (§15)
|
||||
// In FCFS mode, the closest processor to the recipient processes the job
|
||||
func SelectProcessorByProximity(recipientLat, recipientLng float64, processors []Processor) *Processor {
|
||||
if len(processors) == 0 {
|
||||
return nil
|
||||
}
|
||||
closest := &processors[0]
|
||||
minDist := haversine(recipientLat, recipientLng, closest.Latitude, closest.Longitude)
|
||||
for i := 1; i < len(processors); i++ {
|
||||
dist := haversine(recipientLat, recipientLng, processors[i].Latitude, processors[i].Longitude)
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
closest = &processors[i]
|
||||
}
|
||||
}
|
||||
return closest
|
||||
}
|
||||
|
||||
// haversine computes distance between two coordinates (simplified)
|
||||
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
dlat := lat2 - lat1
|
||||
dlng := lng2 - lng1
|
||||
return dlat*dlat + dlng*dlng // squared distance (sufficient for comparison)
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Processors []Processor `json:"processors" yaml:"processors"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Processors: []Processor{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,43 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "rootpool"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// RootPool is the mesh's treasury (§5)
|
||||
// Attested by Watchers, visible to every Holder.
|
||||
type RootPool struct {
|
||||
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||
LastAttestation int64 `json:"last_attestation" yaml:"last_attestation"`
|
||||
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
|
||||
ReserveRatioPct uint32 `json:"reserve_ratio_pct" yaml:"reserve_ratio_pct"`
|
||||
}
|
||||
|
||||
// RootPoolEntry tracks incoming/outgoing Root-Pool movements
|
||||
type RootPoolEntry struct {
|
||||
EntryID string `json:"entry_id" yaml:"entry_id"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Amount int64 `json:"amount" yaml:"amount"`
|
||||
Reason string `json:"reason" yaml:"reason"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
RootPool RootPool `json:"root_pool" yaml:"root_pool"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams()}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,160 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "standing"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// Standing formula parameters (§9.2 — LOCKED)
|
||||
PriorMean = 4.0 // Bayesian prior mean
|
||||
PriorWeight = 10 // Bayesian prior weight
|
||||
DecayBucket6mo = 1.0 // 6mo: 100pct
|
||||
DecayBucket12mo = 0.5 // 12mo: 50pct
|
||||
DecayBucket24mo = 0.25 // 24mo: 25pct
|
||||
DecayBucket24plus = 0.0 // 24mo+: 0pct
|
||||
|
||||
// Diversity bonus (§9.2)
|
||||
DiversityBonus3Cats = 0.05
|
||||
DiversityBonus4Cats = 0.10
|
||||
DiversityBonus5Cats = 0.15
|
||||
|
||||
// Voucher weights (§9.2)
|
||||
VoucherWeightFreeholder = 1.5
|
||||
VoucherWeight45Plus = 1.2
|
||||
VoucherWeight40To45 = 1.0
|
||||
VoucherWeightBelow40 = 0.5
|
||||
VoucherWeightBelow10Ratings = 0.3
|
||||
|
||||
// Minimum counterparties (§9.2)
|
||||
MinCounterpartiesValid = 3 // to be valid
|
||||
MinCounterpartiesFreeholderEligible = 10 // to be Freeholder-eligible
|
||||
MinCounterpartiesFreeholderStatus = 30 // across 3 categories for Freeholder status
|
||||
|
||||
// Freeholder Four Signals (§9.1 — LOCKED)
|
||||
FreeholderStashMaturityDays = 90 // 90 days Stash activity
|
||||
FreeholderStashMaxGapDays = 30 // no gap > 30 days
|
||||
FreeholderMinStandingScore = 4.5 // 4.5+ in at least 3 service categories
|
||||
FreeholderMinCategories = 3 // at least 3 service categories
|
||||
)
|
||||
|
||||
// Rating is a single rating event (§9.2)
|
||||
type Rating struct {
|
||||
RaterID string `json:"rater_id" yaml:"rater_id"`
|
||||
RateeID string `json:"ratee_id" yaml:"ratee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
Score float64 `json:"score" yaml:"score"` // 0.0-5.0
|
||||
Weight float64 `json:"weight" yaml:"weight"` // derived from rater Voucher_Weight
|
||||
TxRef string `json:"tx_ref" yaml:"tx_ref"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
DecayBucket uint8 `json:"decay_bucket" yaml:"decay_bucket"`
|
||||
}
|
||||
|
||||
// Vouch is a Freeholder vouch with skin-in-the-game (§9.1)
|
||||
type Vouch struct {
|
||||
VoucherID string `json:"voucher_id" yaml:"voucher_id"`
|
||||
VoucheeID string `json:"vouchee_id" yaml:"vouchee_id"`
|
||||
Category string `json:"category" yaml:"category"`
|
||||
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` // voucher skin-in-the-game
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// Slash penalizes a Holder (§9.4)
|
||||
type Slash struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
Amount float64 `json:"amount" yaml:"amount"`
|
||||
Reason string `json:"reason" yaml:"reason"` // Crack, FraudulentCoverCall, InactivityTimeout
|
||||
Attester string `json:"attester" yaml:"attester"` // Watcher ID
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// StandingBucket for display (§9.2)
|
||||
type StandingBucket string
|
||||
|
||||
const (
|
||||
BucketNew StandingBucket = "New" // <10 ratings
|
||||
BucketTrusted StandingBucket = "Trusted" // 4.0-4.4
|
||||
BucketPreferred StandingBucket = "Preferred" // 4.5-4.7
|
||||
BucketTop StandingBucket = "Top" // 4.8+
|
||||
BucketSlashed StandingBucket = "Slashed" // penalized
|
||||
)
|
||||
|
||||
// FreeholderSignals — all four must be present (§9.1 LOCKED)
|
||||
type FreeholderSignals struct {
|
||||
StashMaturity bool `json:"stash_maturity" yaml:"stash_maturity"`
|
||||
MultiDomainStanding bool `json:"multi_domain_standing" yaml:"multi_domain_standing"`
|
||||
CommittedCapital bool `json:"committed_capital" yaml:"committed_capital"`
|
||||
CommunityEndorsement bool `json:"community_endorsement" yaml:"community_endorsement"`
|
||||
}
|
||||
|
||||
// IsFreeholderEligible checks all four signals (§9.1)
|
||||
// No application, no committee, no form. All four must be present.
|
||||
func (s FreeholderSignals) IsFreeholderEligible() bool {
|
||||
return s.StashMaturity && s.MultiDomainStanding && s.CommittedCapital && s.CommunityEndorsement
|
||||
}
|
||||
|
||||
// ComputeDiversityBonus returns the bonus for category count (§9.2)
|
||||
func ComputeDiversityBonus(categoryCount int) float64 {
|
||||
switch {
|
||||
case categoryCount >= 5:
|
||||
return DiversityBonus5Cats
|
||||
case categoryCount == 4:
|
||||
return DiversityBonus4Cats
|
||||
case categoryCount == 3:
|
||||
return DiversityBonus3Cats
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// GetVoucherWeight returns the weight for a given rater profile (§9.2)
|
||||
func GetVoucherWeight(isFreeholder bool, standingScore float64, ratingCount int) float64 {
|
||||
if isFreeholder {
|
||||
return VoucherWeightFreeholder
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return VoucherWeightBelow10Ratings
|
||||
}
|
||||
if standingScore >= 4.5 {
|
||||
return VoucherWeight45Plus
|
||||
}
|
||||
if standingScore >= 4.0 {
|
||||
return VoucherWeight40To45
|
||||
}
|
||||
return VoucherWeightBelow40
|
||||
}
|
||||
|
||||
// GetStandingBucket returns the display bucket for a score (§9.2)
|
||||
func GetStandingBucket(score float64, ratingCount int, isSlashed bool) StandingBucket {
|
||||
if isSlashed {
|
||||
return BucketSlashed
|
||||
}
|
||||
if ratingCount < 10 {
|
||||
return BucketNew
|
||||
}
|
||||
switch {
|
||||
case score >= 4.8:
|
||||
return BucketTop
|
||||
case score >= 4.5:
|
||||
return BucketPreferred
|
||||
case score >= 4.0:
|
||||
return BucketTrusted
|
||||
}
|
||||
return BucketNew
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Ratings []Rating `json:"ratings" yaml:"ratings"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Ratings: []Rating{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,100 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/standing/types"
|
||||
)
|
||||
|
||||
func TestFreeholderSignalsAllPresent(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: true,
|
||||
}
|
||||
if !signals.IsFreeholderEligible() {
|
||||
t.Error("All four signals present should be Freeholder eligible (§9.1)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreeholderSignalsMissing(t *testing.T) {
|
||||
signals := types.FreeholderSignals{
|
||||
StashMaturity: true,
|
||||
MultiDomainStanding: true,
|
||||
CommittedCapital: true,
|
||||
CommunityEndorsement: false, // missing
|
||||
}
|
||||
if signals.IsFreeholderEligible() {
|
||||
t.Error("Missing one signal should NOT be Freeholder eligible (§9.1: all four must be present)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiversityBonus(t *testing.T) {
|
||||
if types.ComputeDiversityBonus(3) != 0.05 {
|
||||
t.Error("3 categories should give +0.05 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(4) != 0.10 {
|
||||
t.Error("4 categories should give +0.10 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(5) != 0.15 {
|
||||
t.Error("5+ categories should give +0.15 bonus (§9.2)")
|
||||
}
|
||||
if types.ComputeDiversityBonus(2) != 0.0 {
|
||||
t.Error("2 categories should give 0 bonus (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoucherWeights(t *testing.T) {
|
||||
if types.GetVoucherWeight(true, 4.0, 100) != 1.5 {
|
||||
t.Error("Freeholder weight should be 1.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.6, 100) != 1.2 {
|
||||
t.Error("4.5+ with 1-2 cats should be 1.2x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.2, 100) != 1.0 {
|
||||
t.Error("4.0-4.5 should be 1.0x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 3.5, 100) != 0.5 {
|
||||
t.Error("Below 4.0 should be 0.5x (§9.2)")
|
||||
}
|
||||
if types.GetVoucherWeight(false, 4.0, 5) != 0.3 {
|
||||
t.Error("Below 10 ratings should be 0.3x (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandingBuckets(t *testing.T) {
|
||||
if types.GetStandingBucket(4.9, 100, false) != types.BucketTop {
|
||||
t.Error("4.8+ should be Top (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.6, 100, false) != types.BucketPreferred {
|
||||
t.Error("4.5-4.7 should be Preferred (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.2, 100, false) != types.BucketTrusted {
|
||||
t.Error("4.0-4.4 should be Trusted (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(3.5, 100, false) != types.BucketNew {
|
||||
t.Error("Below 4.0 should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 5, false) != types.BucketNew {
|
||||
t.Error("Below 10 ratings should be New (§9.2)")
|
||||
}
|
||||
if types.GetStandingBucket(4.5, 100, true) != types.BucketSlashed {
|
||||
t.Error("Slashed should be Slashed bucket (§9.2)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockedConstants(t *testing.T) {
|
||||
if types.FreeholderStashMaturityDays != 90 {
|
||||
t.Error("Stash maturity should be 90 days (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinStandingScore != 4.5 {
|
||||
t.Error("Min standing score should be 4.5 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.FreeholderMinCategories != 3 {
|
||||
t.Error("Min categories should be 3 (§9.1 LOCKED)")
|
||||
}
|
||||
if types.MinCounterpartiesFreeholderStatus != 30 {
|
||||
t.Error("Min counterparties for Freeholder status should be 30 (§9.2)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "stash"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Stash is a per-Holder sovereign storage (§5)
|
||||
// The Holder owns it. No one else can access it without a Window (§10).
|
||||
type Stash struct {
|
||||
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
LastActive int64 `json:"last_active" yaml:"last_active"`
|
||||
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||
IsStill bool `json:"is_still" yaml:"is_still"` // Still = Holder paused partner access (§10)
|
||||
}
|
||||
|
||||
// StashActivity tracks Stash maturity for Freeholder signals (§9.1: 90 days, no gap >30 days)
|
||||
type StashActivity struct {
|
||||
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||
ActiveDays uint32 `json:"active_days" yaml:"active_days"`
|
||||
MaxGapDays uint32 `json:"max_gap_days" yaml:"max_gap_days"` // must be ≤30 for Freeholder
|
||||
LastActivityDay int64 `json:"last_activity_day" yaml:"last_activity_day"`
|
||||
}
|
||||
|
||||
// MaturityThreshold for Freeholder signal (§9.1: 90 days)
|
||||
const MaturityThresholdDays = 90
|
||||
|
||||
// MaxGapForMaturity for Freeholder signal (§9.1: no gap > 30 days)
|
||||
const MaxGapForMaturity = 30
|
||||
|
||||
// IsMature checks if Stash activity meets the Freeholder maturity threshold (§9.1)
|
||||
func (a StashActivity) IsMature() bool {
|
||||
return a.ActiveDays >= MaturityThresholdDays && a.MaxGapDays <= MaxGapForMaturity
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Stashes []Stash `json:"stashes" yaml:"stashes"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Stashes: []Stash{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,43 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/stash/types"
|
||||
)
|
||||
|
||||
func TestMaturityThreshold(t *testing.T) {
|
||||
if types.MaturityThresholdDays != 90 {
|
||||
t.Errorf("MaturityThresholdDays = %d, expected 90 (§9.1)", types.MaturityThresholdDays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxGapForMaturity(t *testing.T) {
|
||||
if types.MaxGapForMaturity != 30 {
|
||||
t.Errorf("MaxGapForMaturity = %d, expected 30 (§9.1: no gap > 30 days)", types.MaxGapForMaturity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashIsMature(t *testing.T) {
|
||||
mature := types.StashActivity{ActiveDays: 90, MaxGapDays: 15}
|
||||
if !mature.IsMature() {
|
||||
t.Error("90 days active, 15 max gap should be mature (§9.1)")
|
||||
}
|
||||
|
||||
notEnoughDays := types.StashActivity{ActiveDays: 89, MaxGapDays: 15}
|
||||
if notEnoughDays.IsMature() {
|
||||
t.Error("89 days should NOT be mature (threshold = 90)")
|
||||
}
|
||||
|
||||
gapTooBig := types.StashActivity{ActiveDays: 100, MaxGapDays: 31}
|
||||
if gapTooBig.IsMature() {
|
||||
t.Error("31-day gap should NOT be mature (max gap = 30)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashStillFlag(t *testing.T) {
|
||||
stash := types.Stash{IsStill: true}
|
||||
if !stash.IsStill {
|
||||
t.Error("Stash with IsStill=true should reflect Still state (§10: Holder paused partner access)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "still"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// StillState represents the pause/resume state (§3: Still/Stir)
|
||||
type StillState string
|
||||
|
||||
const (
|
||||
// StirActive — normal operation, Forge/Fold enabled
|
||||
StirActive StillState = "StirActive"
|
||||
// StillPaused — Forge/Fold blocked until Stir
|
||||
StillPaused StillState = "StillPaused"
|
||||
)
|
||||
|
||||
// StillReason defines why the system was stilled
|
||||
type StillReason string
|
||||
|
||||
const (
|
||||
StillCouncilVote StillReason = "CouncilVote"
|
||||
StillQuorumLost StillReason = "QuorumLost"
|
||||
StillSecurityAlert StillReason = "SecurityAlert"
|
||||
)
|
||||
|
||||
// StillRecord tracks a pause event
|
||||
type StillRecord struct {
|
||||
State StillState `json:"state" yaml:"state"`
|
||||
Reason StillReason `json:"reason" yaml:"reason"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
TriggeredBy string `json:"triggered_by" yaml:"triggered_by"`
|
||||
}
|
||||
|
||||
// Params for the still module
|
||||
type Params struct {
|
||||
AutoStillOnQuorumLost bool `json:"auto_still_on_quorum_lost" yaml:"auto_still_on_quorum_lost"`
|
||||
}
|
||||
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
AutoStillOnQuorumLost: true, // §4.2: Forge/Fold blocked until quorum restored
|
||||
}
|
||||
}
|
||||
|
||||
// GenesisState defines the still module genesis state
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
State *StillRecord `json:"state" yaml:"state"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
State: &StillRecord{State: StirActive},
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,30 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/still/types"
|
||||
)
|
||||
|
||||
func TestDefaultStillState(t *testing.T) {
|
||||
state := types.DefaultGenesisState()
|
||||
if state.State.State != types.StirActive {
|
||||
t.Errorf("Default state = %s, expected StirActive", state.State.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoStillOnQuorumLost(t *testing.T) {
|
||||
params := types.DefaultParams()
|
||||
if !params.AutoStillOnQuorumLost {
|
||||
t.Error("AutoStillOnQuorumLost should be true (§4.2: Forge/Fold blocked until quorum restored)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStillStates(t *testing.T) {
|
||||
if types.StirActive != "StirActive" {
|
||||
t.Errorf("StirActive = %s, expected 'StirActive'", types.StirActive)
|
||||
}
|
||||
if types.StillPaused != "StillPaused" {
|
||||
t.Errorf("StillPaused = %s, expected 'StillPaused'", types.StillPaused)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
ModuleName = "vault"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
)
|
||||
|
||||
// Vault is a per-Stand governed storage (§5)
|
||||
// Governed by the Stand Council (§19). Holds shared funds for a Stand.
|
||||
type Vault struct {
|
||||
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||
}
|
||||
|
||||
// VaultAllocation tracks sub-Vault allocations within a Stand
|
||||
type VaultAllocation struct {
|
||||
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||
SubVaultID string `json:"sub_vault_id" yaml:"sub_vault_id"`
|
||||
Percentage uint32 `json:"percentage" yaml:"percentage"`
|
||||
Purpose string `json:"purpose" yaml:"purpose"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Vaults []Vault `json:"vaults" yaml:"vaults"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{Params: DefaultParams(), Vaults: []Vault{}}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,120 @@
|
||||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
// ModuleName is the name of the watcher module
|
||||
ModuleName = "watcher"
|
||||
|
||||
// StoreKey is the string store key
|
||||
StoreKey = ModuleName
|
||||
|
||||
// RouterKey is the message route for the watcher module
|
||||
RouterKey = ModuleName
|
||||
|
||||
// QuerierRoute is the querier route for the watcher module
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// MinBond is the minimum bond for a Watcher: 100,000 Bread (§7)
|
||||
// Bread atomic unit = Grain; 1 Bread = 10,000 Grain
|
||||
MinBond = 100000
|
||||
|
||||
// Quorum is the minimum attestation count: 6-of-9 (§7)
|
||||
Quorum = 6
|
||||
|
||||
// MaxWatchers is the maximum active Watcher count: 9 (§7)
|
||||
MaxWatchers = 9
|
||||
|
||||
// AttestationEpochHours is the daily attestation window (§7)
|
||||
AttestationEpochHours = 24
|
||||
)
|
||||
|
||||
// Parameter store keys
|
||||
var (
|
||||
KeyActiveWatchers = []byte("ActiveWatchers")
|
||||
KeyBondAmount = []byte("BondAmount")
|
||||
)
|
||||
|
||||
// Watcher represents a bonded Watcher (§7)
|
||||
type Watcher struct {
|
||||
WatcherID string `json:"watcher_id" yaml:"watcher_id"`
|
||||
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"`
|
||||
Jurisdiction string `json:"jurisdiction" yaml:"jurisdiction"`
|
||||
Organization string `json:"organization" yaml:"organization"`
|
||||
ActiveSince int64 `json:"active_since" yaml:"active_since"`
|
||||
Slashes uint32 `json:"slashes" yaml:"slashes"`
|
||||
LastAttestation int64 `json:"last_attestation" yaml:"last_attestation"`
|
||||
}
|
||||
|
||||
// Attestation is a daily reserve snapshot signed by a Watcher (§7)
|
||||
type Attestation struct {
|
||||
WatcherID string `json:"watcher_id" yaml:"watcher_id"`
|
||||
Snapshot ReserveSnapshot `json:"snapshot" yaml:"snapshot"`
|
||||
Signature []byte `json:"signature" yaml:"signature"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// ReserveSnapshot is the off-chain reserve state attested by Watchers (§7)
|
||||
type ReserveSnapshot struct {
|
||||
OffChainTotal int64 `json:"off_chain_total" yaml:"off_chain_total"`
|
||||
OnChainBread int64 `json:"on_chain_bread" yaml:"on_chain_bread"`
|
||||
BasketBreakdown []BasketAsset `json:"basket_breakdown" yaml:"basket_breakdown"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
AuditorRef string `json:"auditor_ref" yaml:"auditor_ref"`
|
||||
}
|
||||
|
||||
// BasketAsset represents one asset in the Root Basket (§6)
|
||||
type BasketAsset struct {
|
||||
AssetType string `json:"asset_type" yaml:"asset_type"`
|
||||
Amount int64 `json:"amount" yaml:"amount"`
|
||||
}
|
||||
|
||||
// QuorumStatus represents the current quorum state
|
||||
type QuorumStatus string
|
||||
|
||||
const (
|
||||
QuorumMet QuorumStatus = "QuorumMet" // ≥6 valid sigs in last epoch
|
||||
QuorumLost QuorumStatus = "QuorumLost" // <6 — Forge/Fold stilled
|
||||
StaleEpoch QuorumStatus = "StaleEpoch" // no attestations in 24h
|
||||
)
|
||||
|
||||
// SlashReason defines why a Watcher was slashed
|
||||
type SlashReason string
|
||||
|
||||
const (
|
||||
SlashInsufficientBond SlashReason = "InsufficientBond"
|
||||
SlashStaleAttestation SlashReason = "StaleAttestation"
|
||||
SlashGeographicCollusion SlashReason = "GeographicCollusion"
|
||||
SlashForgery SlashReason = "Forgery"
|
||||
)
|
||||
|
||||
// Params defines the parameters for the watcher module
|
||||
type Params struct {
|
||||
MinBondAmount int64 `json:"min_bond_amount" yaml:"min_bond_amount"`
|
||||
QuorumCount uint32 `json:"quorum_count" yaml:"quorum_count"`
|
||||
MaxWatchers uint32 `json:"max_watchers" yaml:"max_watchers"`
|
||||
}
|
||||
|
||||
// DefaultParams returns default watcher module parameters
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
MinBondAmount: int64(MinBond) * 10000, // 100,000 Bread in Grain
|
||||
QuorumCount: Quorum,
|
||||
MaxWatchers: MaxWatchers,
|
||||
}
|
||||
}
|
||||
|
||||
// GenesisState defines the watcher module genesis state
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Watchers []Watcher `json:"watchers" yaml:"watchers"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Watchers: []Watcher{},
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||
@@ -0,0 +1,41 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/watcher/types"
|
||||
)
|
||||
|
||||
func TestMinBond(t *testing.T) {
|
||||
if types.MinBond != 100000 {
|
||||
t.Errorf("MinBond = %d, expected 100000 (§7: 100,000 Bread bond)", types.MinBond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorum(t *testing.T) {
|
||||
if types.Quorum != 6 {
|
||||
t.Errorf("Quorum = %d, expected 6 (§7: 6-of-9 quorum)", types.Quorum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxWatchers(t *testing.T) {
|
||||
if types.MaxWatchers != 9 {
|
||||
t.Errorf("MaxWatchers = %d, expected 9 (§7: 9 active Watchers)", types.MaxWatchers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttestationEpoch(t *testing.T) {
|
||||
if types.AttestationEpochHours != 24 {
|
||||
t.Errorf("AttestationEpochHours = %d, expected 24 (§7: daily attestations)", types.AttestationEpochHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
params := types.DefaultParams()
|
||||
if params.QuorumCount != 6 {
|
||||
t.Errorf("DefaultParams QuorumCount = %d, expected 6", params.QuorumCount)
|
||||
}
|
||||
if params.MaxWatchers != 9 {
|
||||
t.Errorf("DefaultParams MaxWatchers = %d, expected 9", params.MaxWatchers)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user