diff --git a/cmd/main.go b/cmd/main.go index 737cd86..c37a9c6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,6 +16,7 @@ import ( httpremote "git.grassecon.net/grassrootseconomics/sarafu-api/remote/http" devremote "git.grassecon.net/grassrootseconomics/sarafu-api/dev" "git.grassecon.net/grassrootseconomics/sarafu-api/remote" + "git.grassecon.net/grassrootseconomics/sarafu-api/event" "git.grassecon.net/grassrootseconomics/sarafu-vise/args" "git.grassecon.net/grassrootseconomics/sarafu-vise/handlers" ) @@ -26,6 +27,24 @@ var ( menuSeparator = ": " ) +// WIP: placeholder emitter, should perform same action as events +func emitter(ctx context.Context, msg event.Msg) error { + if msg.Typ == event.EventTokenTransferTag { + tx, ok := msg.Item.(devremote.Tx) + if !ok { + return fmt.Errorf("not a valid tx") + } + logg.InfoCtxf(ctx, "tx emit", "tx", tx) + } else if msg.Typ == event.EventRegistrationTag { + acc, ok := msg.Item.(devremote.Account) + if !ok { + return fmt.Errorf("not a valid tx") + } + logg.InfoCtxf(ctx, "account emit", "account", acc) + + } + return nil +} func main() { config.LoadConfig() @@ -129,7 +148,7 @@ func main() { } if fakeDir != "" { - svc := devremote.NewDevAccountService(ctx, fakeDir).WithAutoVoucher(ctx, "FOO", 42) + svc := devremote.NewDevAccountService(ctx, fakeDir).WithAutoVoucher(ctx, "FOO", 42).WithEmitter(emitter) svc.AddVoucher(ctx, "BAR") accountService = svc } else { diff --git a/handlers/event/custodial.go b/handlers/event/custodial.go new file mode 100644 index 0000000..b0c6c99 --- /dev/null +++ b/handlers/event/custodial.go @@ -0,0 +1,33 @@ +package event + +import ( + "context" + + "git.defalsify.org/vise.git/persist" + apievent "git.grassecon.net/grassrootseconomics/sarafu-api/event" + "git.grassecon.net/grassrootseconomics/sarafu-vise/store" +) + +const ( + // TODO: consolidate with loaded flags + accountCreatedFlag = 9 +) + +// handle custodial registration. +// +// TODO: implement account created in userstore instead, so that +// the need for persister and state use here is eliminated (it +// introduces concurrency risks) +func (eh *EventsHandler) HandleCustodialRegistration(ctx context.Context, userStore *store.UserDataStore, pr *persist.Persister, ev *apievent.EventCustodialRegistration) error { + identity, err := store.IdentityFromAddress(ctx, userStore, ev.Account) + if err != nil { + return err + } + err = pr.Load(identity.SessionId) + if err != nil { + return err + } + st := pr.GetState() + st.SetFlag(accountCreatedFlag) + return pr.Save(identity.SessionId) +} diff --git a/handlers/event/event.go b/handlers/event/event.go new file mode 100644 index 0000000..86b31aa --- /dev/null +++ b/handlers/event/event.go @@ -0,0 +1,16 @@ +package event + +import ( + "git.grassecon.net/grassrootseconomics/sarafu-api/remote" +) + +type EventsHandler struct { + api remote.AccountService + formatFunc func(string, int, any) string +} + +func NewEventsHandler(api remote.AccountService) *EventsHandler { + return &EventsHandler{ + api: api, + } +} diff --git a/handlers/event/token.go b/handlers/event/token.go new file mode 100644 index 0000000..bf5075e --- /dev/null +++ b/handlers/event/token.go @@ -0,0 +1,183 @@ +package event + +import ( + "context" + "strings" + + "git.defalsify.org/vise.git/db" + "git.grassecon.net/grassrootseconomics/sarafu-vise/store" + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + "git.grassecon.net/grassrootseconomics/common/identity" + apievent "git.grassecon.net/grassrootseconomics/sarafu-api/event" +) + +// execute all +func (eh *EventsHandler) updateToken(ctx context.Context, userStore *store.UserDataStore, identity identity.Identity, tokenAddress string) error { + err := eh.updateTokenList(ctx, userStore, identity) + if err != nil { + return err + } + + userStore.Db.SetSession(identity.SessionId) + activeSym, err := userStore.ReadEntry(ctx, identity.SessionId, storedb.DATA_ACTIVE_SYM) + if err == nil { + return nil + } + if !db.IsNotFound(err) { + return err + } + if activeSym == nil { + activeSym, err = eh.toSym(ctx, tokenAddress) + if err != nil { + return err + } + } + + err = updateDefaultToken(ctx, userStore, identity, string(activeSym)) + if err != nil { + return err + } + + err = eh.updateTokenTransferList(ctx, userStore, identity) + if err != nil { + return err + } + + return nil +} + + +// set default token to given symbol. +func updateDefaultToken(ctx context.Context, userStore *store.UserDataStore, identity identity.Identity, activeSym string) error { + pfxDb := toPrefixDb(userStore, identity.SessionId) + // TODO: the activeSym input should instead be newline separated list? + tokenData, err := store.GetVoucherData(ctx, pfxDb, activeSym) + if err != nil { + return err + } + return store.UpdateVoucherData(ctx, userStore, identity.SessionId, tokenData) +} + + +// handle token transfer. +// +// if from and to are NOT the same, handle code will be executed once for each side of the transfer. +func (eh *EventsHandler) HandleTokenTransfer(ctx context.Context, userStore *store.UserDataStore, ev *apievent.EventTokenTransfer) error { + identity, err := store.IdentityFromAddress(ctx, userStore, ev.From) + if err != nil { + if !db.IsNotFound(err) { + return err + } + } else { + err = eh.updateToken(ctx, userStore, identity, ev.VoucherAddress) + if err != nil { + return err + } + } + + if strings.Compare(ev.To, ev.From) != 0 { + identity, err = store.IdentityFromAddress(ctx, userStore, ev.To) + if err != nil { + if !db.IsNotFound(err) { + return err + } + } else { + err = eh.updateToken(ctx, userStore, identity, ev.VoucherAddress) + if err != nil { + return err + } + } + } + + return nil +} + +// handle token mint. +func (eh *EventsHandler) HandleTokenMint(ctx context.Context, userStore *store.UserDataStore, ev *apievent.EventTokenMint) error { + identity, err := store.IdentityFromAddress(ctx, userStore, ev.To) + if err != nil { + if !db.IsNotFound(err) { + return err + } + } else { + err = eh.updateToken(ctx, userStore, identity, ev.VoucherAddress) + if err != nil { + return err + } + } + return nil +} + +// use api to resolve address to token symbol. +func (ev *EventsHandler) toSym(ctx context.Context, address string) ([]byte, error) { + voucherData, err := ev.api.VoucherData(ctx, address) + if err != nil { + return nil, err + } + return []byte(voucherData.TokenSymbol), nil +} + +// refresh and store token list. +func (eh *EventsHandler) updateTokenList(ctx context.Context, userStore *store.UserDataStore, identity identity.Identity) error { + holdings, err := eh.api.FetchVouchers(ctx, identity.ChecksumAddress) + if err != nil { + return err + } + metadata := store.ProcessVouchers(holdings) + _ = metadata + + // TODO: make sure subprefixdb is thread safe when using gdbm + // TODO: why is address session here unless explicitly set + pfxDb := toPrefixDb(userStore, identity.SessionId) + + typ := storedb.ToBytes(storedb.DATA_VOUCHER_SYMBOLS) + err = pfxDb.Put(ctx, typ, []byte(metadata.Symbols)) + if err != nil { + return err + } + + typ = storedb.ToBytes(storedb.DATA_VOUCHER_BALANCES) + err = pfxDb.Put(ctx, typ, []byte(metadata.Balances)) + if err != nil { + return err + } + + typ = storedb.ToBytes(storedb.DATA_VOUCHER_DECIMALS) + err = pfxDb.Put(ctx, typ, []byte(metadata.Decimals)) + if err != nil { + return err + } + + typ = storedb.ToBytes(storedb.DATA_VOUCHER_ADDRESSES) + err = pfxDb.Put(ctx, typ, []byte(metadata.Addresses)) + if err != nil { + return err + } + + return nil +} + +// refresh and store transaction history. +func (eh *EventsHandler) updateTokenTransferList(ctx context.Context, userStore *store.UserDataStore, identity identity.Identity) error { + var r []string + + txs, err := eh.api.FetchTransactions(ctx, identity.ChecksumAddress) + if err != nil { + return err + } + + for i, tx := range(txs) { + //r = append(r, formatTransaction(i, tx)) + r = append(r, eh.formatFunc(apievent.EventTokenTransferTag, i, tx)) + } + + s := strings.Join(r, "\n") + + return userStore.WriteEntry(ctx, identity.SessionId, storedb.DATA_TRANSACTIONS, []byte(s)) +} + +func toPrefixDb(userStore *store.UserDataStore, sessionId string) storedb.PrefixDb { + userStore.Db.SetSession(sessionId) + prefix := storedb.ToBytes(db.DATATYPE_USERDATA) + return store.StoreToPrefixDb(userStore, prefix) +} diff --git a/store/user_store.go b/store/user_store.go index 920d32e..70e2539 100644 --- a/store/user_store.go +++ b/store/user_store.go @@ -6,6 +6,8 @@ import ( visedb "git.defalsify.org/vise.git/db" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + "git.grassecon.net/grassrootseconomics/common/hex" + "git.grassecon.net/grassrootseconomics/common/identity" ) // TODO: Rename interface, "datastore" is redundant naming and too general @@ -39,3 +41,37 @@ func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, ty func StoreToPrefixDb(userStore *UserDataStore, pfx []byte) storedb.PrefixDb { return storedb.NewSubPrefixDb(userStore.Db, pfx) } + +// IdentityFromAddress fully populates and Identity object from a given +// checksum address. +// +// It is the caller's responsibility to ensure that a valid checksum address +// is passed. +func IdentityFromAddress(ctx context.Context, userStore *UserDataStore, address string) (identity.Identity, error) { + var err error + var ident identity.Identity + + ident.ChecksumAddress = address + ident.NormalAddress, err = hex.NormalizeHex(ident.ChecksumAddress) + if err != nil { + return ident, err + } + ident.SessionId, err = getSessionIdByAddress(ctx, userStore, ident.NormalAddress) + if err != nil { + return ident, err + } + return ident, nil +} + +// load matching session from address from db store. +func getSessionIdByAddress(ctx context.Context, userStore *UserDataStore, address string) (string, error) { + // TODO: replace with userdatastore when double sessionid issue fixed + //r, err := store.ReadEntry(ctx, address, common.DATA_PUBLIC_KEY_REVERSE) + userStore.Db.SetPrefix(visedb.DATATYPE_USERDATA) + userStore.Db.SetSession(address) + r, err := userStore.Db.Get(ctx, storedb.PackKey(storedb.DATA_PUBLIC_KEY_REVERSE, []byte{})) + if err != nil { + return "", err + } + return string(r), nil +}