Skip to content

Commit 9521ec1

Browse files
authored
observer notify mixin messenger user of new transaction
* add notification table * keep records when mtg transfer assets to user * send notifications when tx finished * fix sql * fix mint authority may not exist * Remove unused fields
1 parent 178a0c1 commit 9521ec1

File tree

8 files changed

+182
-2
lines changed

8 files changed

+182
-2
lines changed

apps/solana/rpc.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,19 @@ func (c *Client) RPCGetAsset(ctx context.Context, address string) (*Asset, error
114114
panic(err)
115115
}
116116

117+
mintAuthority := ""
117118
freezeAuthority := ""
118119
if mint.Parsed.Info.FreezeAuthority != nil {
119120
freezeAuthority = mint.Parsed.Info.FreezeAuthority.String()
120121
}
122+
if mint.Parsed.Info.MintAuthority != nil {
123+
mintAuthority = mint.Parsed.Info.MintAuthority.String()
124+
}
121125
asset := &Asset{
122126
Address: address,
123127
Id: GenerateAssetId(address),
124128
Decimals: uint32(mint.Parsed.Info.Decimals),
125-
MintAuthority: mint.Parsed.Info.MintAuthority.String(),
129+
MintAuthority: mintAuthority,
126130
FreezeAuthority: freezeAuthority,
127131
}
128132
return asset, nil

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/MixinNetwork/bot-api-go-client/v3 v3.16.9
77
github.com/MixinNetwork/mixin v0.18.27
88
github.com/MixinNetwork/multi-party-sig v0.4.1
9-
github.com/MixinNetwork/safe v0.20.2
9+
github.com/MixinNetwork/safe v0.20.3-0.20250820131619-192be2309dc0
1010
github.com/blocto/solana-go-sdk v1.30.0
1111
github.com/chai2010/webp v1.4.0
1212
github.com/davecgh/go-spew v1.1.1
@@ -71,6 +71,7 @@ require (
7171
github.com/mattn/go-colorable v0.1.14 // indirect
7272
github.com/mattn/go-isatty v0.0.20 // indirect
7373
github.com/mattn/go-sqlite3 v1.14.30 // indirect
74+
github.com/mdp/qrterminal v1.0.1 // indirect
7475
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
7576
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
7677
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -106,4 +107,5 @@ require (
106107
google.golang.org/appengine v1.6.8 // indirect
107108
google.golang.org/protobuf v1.36.6 // indirect
108109
gopkg.in/yaml.v3 v3.0.1 // indirect
110+
rsc.io/qr v0.2.0 // indirect
109111
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ github.com/MixinNetwork/multi-party-sig v0.4.1 h1:rQdIVSDQQOUMub8ERDV1gbFHxGSD5/
1414
github.com/MixinNetwork/multi-party-sig v0.4.1/go.mod h1:mnZyPutnRV2+E6z3v5TpTb7q4HnS7IplS0yy4dPjVGA=
1515
github.com/MixinNetwork/safe v0.20.2 h1:lakqqCH7DWu5VhOHfQy2gvzP2kjTF9offfCiY2rmJ/U=
1616
github.com/MixinNetwork/safe v0.20.2/go.mod h1:7begBaMawmGEr+Y00I2meSvHx3NIl0Z07Wu1inTWYYI=
17+
github.com/MixinNetwork/safe v0.20.3-0.20250820120050-3e8a5f97ae83 h1:oSqmNJBHS14LtUFMReH7E3UhkVq+sSPA4KWlG/IQsqM=
18+
github.com/MixinNetwork/safe v0.20.3-0.20250820120050-3e8a5f97ae83/go.mod h1:+w0TXwc/XH6doove85LIrlJ33SOTLu6UpZaEMkDvwQo=
19+
github.com/MixinNetwork/safe v0.20.3-0.20250820131619-192be2309dc0 h1:TwRcgjskbKDiKuZoOD0UEkNnjD+bKwE2vwf6uQyr7VA=
20+
github.com/MixinNetwork/safe v0.20.3-0.20250820131619-192be2309dc0/go.mod h1:+w0TXwc/XH6doove85LIrlJ33SOTLu6UpZaEMkDvwQo=
1721
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
1822
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
1923
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
@@ -191,6 +195,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
191195
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
192196
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
193197
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
198+
github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c=
199+
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
194200
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
195201
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
196202
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -416,3 +422,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
416422
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
417423
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
418424
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
425+
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
426+
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

solana/observer.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sync"
1111
"time"
1212

13+
"github.com/MixinNetwork/bot-api-go-client/v3"
1314
solanaApp "github.com/MixinNetwork/computer/apps/solana"
1415
"github.com/MixinNetwork/computer/store"
1516
"github.com/MixinNetwork/mixin/crypto"
@@ -72,6 +73,7 @@ func (node *Node) bootObserver(ctx context.Context, version string) {
7273
go node.addressLookupTableLoop(ctx)
7374

7475
go node.refreshAssetsLoop(ctx)
76+
go node.notificationLoop(ctx)
7577
}
7678

7779
func (node *Node) initMPCKeys(ctx context.Context) error {
@@ -315,6 +317,17 @@ func (node *Node) refreshAssetsLoop(ctx context.Context) {
315317
}
316318
}
317319

320+
func (node *Node) notificationLoop(ctx context.Context) {
321+
for {
322+
err := node.sendTransferNotification(ctx)
323+
if err != nil {
324+
panic(err)
325+
}
326+
327+
time.Sleep(time.Minute)
328+
}
329+
}
330+
318331
func (node *Node) initializeUsers(ctx context.Context) error {
319332
offset := node.ReadPropertyAsTime(ctx, store.UserInitializeTimeKey)
320333
us, err := node.store.ListNewUsersAfter(ctx, offset)
@@ -1002,6 +1015,30 @@ func (node *Node) refreshAssets(ctx context.Context) error {
10021015
return node.store.UpdateExternalAssetsInfo(ctx, as)
10031016
}
10041017

1018+
func (node *Node) sendTransferNotification(ctx context.Context) error {
1019+
ns, err := node.store.ListInitialNotifications(ctx)
1020+
if err != nil {
1021+
return err
1022+
}
1023+
1024+
for _, n := range ns {
1025+
hash := node.group.ReadFinishedTxHashByTraceId(ctx, n.TraceId)
1026+
if hash == "" {
1027+
continue
1028+
}
1029+
_, err := bot.SafeNotifySnapshot(ctx, hash, 0, n.OpponentId, node.SafeUser())
1030+
if err != nil {
1031+
return err
1032+
}
1033+
err = node.store.MarkNotificationDone(ctx, n.TraceId)
1034+
if err != nil {
1035+
return err
1036+
}
1037+
}
1038+
1039+
return nil
1040+
}
1041+
10051042
func (node *Node) checkSufficientBalanceForBurnSystemCall(ctx context.Context, call *store.SystemCall) bool {
10061043
tx, err := solana.TransactionFromBase64(call.Raw)
10071044
if err != nil {

store/call.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ func (s *SQLite3Store) RefundOutputsWithRequest(ctx context.Context, req *Reques
213213
if err != nil {
214214
return err
215215
}
216+
if compaction == "" && len(txs) > 0 {
217+
err = s.writeNotifications(ctx, tx, txs)
218+
if err != nil {
219+
return err
220+
}
221+
}
222+
216223
return tx.Commit()
217224
}
218225

@@ -289,6 +296,13 @@ func (s *SQLite3Store) ConfirmBurnRelatedSystemCallWithRequest(ctx context.Conte
289296
return err
290297
}
291298

299+
if len(txs) > 0 {
300+
err = s.writeNotifications(ctx, tx, txs)
301+
if err != nil {
302+
return err
303+
}
304+
}
305+
292306
return tx.Commit()
293307
}
294308

@@ -362,6 +376,13 @@ func (s *SQLite3Store) FailSystemCallWithRequest(ctx context.Context, req *Reque
362376
return err
363377
}
364378

379+
if compaction == "" && len(txs) > 0 {
380+
err = s.writeNotifications(ctx, tx, txs)
381+
if err != nil {
382+
return err
383+
}
384+
}
385+
365386
return tx.Commit()
366387
}
367388

store/notification.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package store
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/MixinNetwork/safe/common"
11+
"github.com/MixinNetwork/safe/mtg"
12+
)
13+
14+
type Notification struct {
15+
TraceId string
16+
OpponentId string
17+
State byte
18+
CreatedAt time.Time
19+
UpdatedAt time.Time
20+
}
21+
22+
var notificationCols = []string{"trace_id", "opponent_id", "state", "created_at", "updated_at"}
23+
24+
func notificationFromRow(row Row) (*Notification, error) {
25+
var n Notification
26+
err := row.Scan(&n.TraceId, &n.OpponentId, &n.State, &n.CreatedAt, &n.UpdatedAt)
27+
if err == sql.ErrNoRows {
28+
return nil, nil
29+
}
30+
return &n, err
31+
}
32+
33+
func (s *SQLite3Store) writeNotifications(ctx context.Context, tx *sql.Tx, txs []*mtg.Transaction) error {
34+
now := time.Now().UTC()
35+
36+
for _, t := range txs {
37+
if len(t.Receivers) != 1 {
38+
continue
39+
}
40+
vals := []any{t.TraceId, t.Receivers[0], common.RequestStateInitial, now, now}
41+
err := s.execOne(ctx, tx, buildInsertionSQL("tx_notifications", notificationCols), vals...)
42+
if err != nil {
43+
return fmt.Errorf("INSERT tx_notifications %v", err)
44+
}
45+
}
46+
47+
return nil
48+
}
49+
50+
func (s *SQLite3Store) ListInitialNotifications(ctx context.Context) ([]*Notification, error) {
51+
s.mutex.RLock()
52+
defer s.mutex.RUnlock()
53+
54+
query := fmt.Sprintf("SELECT %s FROM tx_notifications WHERE state=? ORDER BY created_at ASC LIMIT 20", strings.Join(notificationCols, ","))
55+
rows, err := s.db.QueryContext(ctx, query, common.RequestStateInitial)
56+
if err != nil {
57+
return nil, err
58+
}
59+
defer rows.Close()
60+
61+
var ns []*Notification
62+
for rows.Next() {
63+
n, err := notificationFromRow(rows)
64+
if err != nil {
65+
return nil, err
66+
}
67+
ns = append(ns, n)
68+
}
69+
return ns, nil
70+
}
71+
72+
func (s *SQLite3Store) MarkNotificationDone(ctx context.Context, traceId string) error {
73+
s.mutex.Lock()
74+
defer s.mutex.Unlock()
75+
76+
tx, err := s.db.BeginTx(ctx, nil)
77+
if err != nil {
78+
return err
79+
}
80+
defer common.Rollback(tx)
81+
82+
query := "UPDATE tx_notifications SET state=?,updated_at=? WHERE trace_id=? AND state=?"
83+
err = s.execOne(ctx, tx, query, common.RequestStateDone, time.Now().UTC(), traceId, common.RequestStateInitial)
84+
if err != nil {
85+
return fmt.Errorf("UPDATE tx_notifications %v", err)
86+
}
87+
88+
return tx.Commit()
89+
}

store/request.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ func (s *SQLite3Store) WriteDepositRequestIfNotExist(ctx context.Context, out *m
122122
return err
123123
}
124124

125+
if compaction == "" && len(txs) > 0 {
126+
err = s.writeNotifications(ctx, tx, txs)
127+
if err != nil {
128+
return err
129+
}
130+
}
131+
125132
return tx.Commit()
126133
}
127134

store/schema.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ CREATE TABLE IF NOT EXISTS address_lookup_tables (
262262
CREATE INDEX IF NOT EXISTS address_lookup_tables_by_table ON address_lookup_tables(lookup_table);
263263

264264

265+
CREATE TABLE IF NOT EXISTS tx_notifications (
266+
trace_id VARCHAR NOT NULL,
267+
opponent_id VARCHAR NOT NULL,
268+
state INTEGER NOT NULL,
269+
created_at TIMESTAMP NOT NULL,
270+
updated_at TIMESTAMP NOT NULL,
271+
PRIMARY KEY ('trace_id')
272+
);
273+
274+
CREATE INDEX IF NOT EXISTS tx_notifications_by_state_createdx ON tx_notifications(state, created_at);
275+
276+
265277
CREATE TABLE IF NOT EXISTS action_results (
266278
output_id VARCHAR NOT NULL,
267279
compaction VARCHAR NOT NULL,

0 commit comments

Comments
 (0)