Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WIP GNO mail (no render) #641

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions examples/gno.land/r/demo/mail/DAPP_VS_SYSTEM.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
This is a very specific app.

But the app could also be envisionned to sit on a system instead.
If mail is a particular type of message, and a mail dapp is a user
of a more universal messaging thing, then it brings some advantages.

This mail app would then take just a few lines.
And it would change the way we do things.

For example, orders in p/demo/releases, DAO orders and so on could be other kind of
messages. This way, it would be possible to search and have generic tools that
operate on all point-to-point messages without reimplementing everything all
the time.

You could search inside the release notes emitted by a certain user,
during a certain year. Or searching at the orders emitted by a DAO that contain
a certain text. You could watch for a certain filter on a certain type of
message coming from some organization. (While this can end-up being costly, we
can imagine ways it can be made profitable, or growth-sustainable, instead)

Therefore it would be possible to realize more interoperable systems.

But not all point-to-point system have the same requirements.
Some are producer-consumer, some are archive systems with a certain retention.
Meaning a certain amount of configuration is required for the dapps.

So it's a complex but interesting question.
26 changes: 26 additions & 0 deletions examples/gno.land/r/demo/mail/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Design choices

At this point

* mailbox automatically and freely created on both sides when a mail is sent.
* sender needs to pay stamps, they would go to the realm which will be managed by a DAO to finance new tool
* mails are unencrypted

I assume people would actually not mind paying for stamps if price is reasonnable.
for example it was atom, at 1atom=12usd, I would be okay to pay 0.25$=0.02atom per
mail sent (The time spent redacting a mail costs more than the stamps; and
it would feel less of a waste when I click the button if it costed money).

Do we still need an admin? Yes. Maybe see p/acl
Is it a public service? I think it can be a DAO. So semi-public?

Encryption is a huge topic for GNO and not possible at the moment.

## Various possible ideas

Things like:

TUI - there is nothing in a Render() now
Respond to mails
Mail attachment

12 changes: 12 additions & 0 deletions examples/gno.land/r/demo/mail/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
GNO mail
a demo

📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪

A mailbox connects you to the GNO mail system.
One day maybe GNO mail v.419 can be
used all over the Cosmos.

📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪 📪


27 changes: 27 additions & 0 deletions examples/gno.land/r/demo/mail/container.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package mail

// A private structure to hold mails
// current impl. is an array
// RFC Would a ringbuffer with a certain capacity, and a "keep" feature
// be better, I wonder.

type container struct {
mails []*Mail
}

func newContainer() *container { return &container{[]*Mail{}} }
func newContainerWith(a []*Mail) *container { return &container{a} }

func (ctr *container) push(mail *Mail) {
ctr.mails = append(ctr.mails, mail)
}

func (ctr *container) filter(preds ...Predicate) []*Mail {
a := []*Mail{}
for _, mail := range ctr.mails {
if mail.Satisfies(preds) {
a = append(a, mail)
}
}
return a
}
83 changes: 83 additions & 0 deletions examples/gno.land/r/demo/mail/files/mail_filetest.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// PKGPATH: gno.land/r/demo/mail_test
package mail_test

import (
"std"
"strconv"
"strings"
"time"

"gno.land/p/demo/avl"
"gno.land/p/demo/testutils"
"gno.land/r/demo/mail"
)

// Send mail, a great movie - SYNOPSIS
// Starring "dude" (the OrigCaller) and "sanders"
// ------------------------------------------------------------------
// Let there be some dude who wants to send mail:
// .dude gets some money, so he can pay for stamps
// .dude sent a mail to sanders
// .now he has a mailbox, so does sanders
// .mail got received by sanders
// .mail also gets saved in the outbox
// .the realm got money from the stamps
// .sanders still has 0gno

// TODO send more emails.
// TODO mail.Id is not checked

func init() {}

func main() {
var (
dude = testutils.TestAddress("dude")
sanders = testutils.TestAddress("sanders")
banker = std.GetBanker(std.BankerTypeReadonly)
)
std.TestSetOrigCaller(dude)
std.TestSetOrigSend(std.Coins{{mail.Fee.Denom, mail.Fee.Amount}}, nil)
coinsBefore := banker.GetCoins(std.GetOrigPkgAddr()).AmountOf(mail.Fee.Denom)
coins := std.Coins{{mail.Fee.Denom, 10 * mail.Fee.Amount}}
std.TestIssueCoins(dude, coins)
if banker.GetCoins(dude).AmountOf(mail.Fee.Denom) == 0 {
panic(".dude luckily gets some money, so he can pay for stamps")
}
// ------------------------------------------
mail.SendMail(sanders, "hi", "this is the dude")
// ------------------------------------------
if !mail.HasAddressMailbox(dude) {
panic(".now he has a mailbox")
}
if !mail.HasAddressMailbox(sanders) {
panic(".and sanders also has a mailbox")
}
dudebox := mail.MustMailbox(dude)
sandersbox := mail.MustMailbox(sanders)
if len(sandersbox.Find(mail.RecipientIs{sanders})) < 1 {
panic(".mail got received by sanders")
}
if len(sandersbox.Find(mail.RecipientIs{sanders}, mail.SenderIs{dude})) < 1 {
panic(".mail got received by sanders")
}
if len(dudebox.Find(mail.SenderIs{dude}, mail.RecipientIs{sanders})) < 1 {
panic(".mail to sanders is in large outbox, by date")
}
if len(sandersbox.ReceivedFrom(dude)) < 1 {
panic(".mail got received by sanders")
}
if len(dudebox.SentTo(sanders)) < 1 {
panic(".mail also saved in outbox")
}
coinsAfter := banker.GetCoins(std.GetOrigPkgAddr()).AmountOf(mail.Fee.Denom)
if coinsAfter-coinsBefore != mail.Fee.Amount {
panic(".realm received the expected amount from the stamps: recvd=" + strconv.Itoa(int(coinsAfter-coinsBefore)))
}
if banker.GetCoins(sanders).AmountOf(mail.Fee.Denom) != 0 {
panic(".sanders still has 0gno")
}
}

// Output:

// Error:
41 changes: 41 additions & 0 deletions examples/gno.land/r/demo/mail/mail.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package mail
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about moving this Mail library logic to examples/gno.land/p/demo/mail, instead of having it sit in a Realm?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Milos!

Thanks taking the time to review this. Yes, deciding what to put in p/ and r/ has been one interrogation of mine.

Here are my thoughts, here is my starting point:

  • p is package, is like library, is common code for everyone to use, meaning it's quite stable.
  • r is realm, is app-like, meaning usage is restricted.

By that logic, code that isn't reasonnably finalized at least during GoR is better kept in r/ as it won't break anything (because nobody will be using it as a dependency).

I think putting things in p/demo is a signal to other devs saying: this code is stable, well-thought out please use it. My app is not there yet, it's WIP.

Another question I have with this app is:

Whether the functionality it offers is really about mails? or about generic messages.
If it's the latter, then the part to put in p/ is not mail, it would be like p/demo/msg, with r/demo/mail being a realm (almost an app instance) using that system.

This tells me it's a bit early to make the separation now.


import (
"std"
"strings"
"time"

"gno.land/p/demo/ufmt"
)

type Mail struct {
id int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should be using an int for a Mail ID - considering this can be something that is potentially used a lot as a message passing realm. Message IDs should probably be something derived from the mail message itself, rather than a counter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither.
But if int in gno means int64, and since 2^63 is 9,223,372,036,854,775,808.
Let's say the system is so popular it generates one million messages per seconds.
It would take 92233720368 seconds to overflow, that is 2924 years.
While it's not perfect (one source claims more than 3 millions email were sent per seconds in 2023) I think this is a bigger discussion.

  • hash(msg): not good IMO (collisions will be trivial to find in the future),
  • the transaction number that produced the message may seem good, except when the system can send a message to several recipients, it will need a suffix to work.

As it's WIP and we have staging testnets I think we can keep an int for the time being.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use int for now and later switch to bigint (#764), maybe.

topic string
body string
time time.Time
sender std.Address
recipient std.Address
}

func newMail(topic, body string, sender std.Address, recipient std.Address) *Mail {
return &Mail{counter + 1, topic, body, time.Now(), sender, recipient}
}

func (mail *Mail) Satisfies(preds []Predicate) bool {
for _, pred := range preds {
if !pred.Satisfy(mail) {
return false
}
}
return true
}

func (mail *Mail) Contains(s string) bool {
s = strings.ToLower(s)
return strings.Contains(strings.ToLower(mail.topic), s) || strings.Contains(strings.ToLower(mail.body), s)
}

// time key for avl, like 1234567890_<counter>
func (mail *Mail) GetTimeKey() string {
return ufmt.Sprintf("%d_%d", time.Now().Unix(), counter)
}
Loading