diff --git a/api/models.go b/api/models.go index b3ae8ca0..fc1aa8af 100644 --- a/api/models.go +++ b/api/models.go @@ -200,15 +200,35 @@ type Transaction struct { Type string `json:"type"` Invoice string `json:"invoice"` Description string `json:"description"` - DescriptionHash string `json:"description_hash"` + DescriptionHash string `json:"descriptionHash"` Preimage *string `json:"preimage"` - PaymentHash string `json:"payment_hash"` + PaymentHash string `json:"paymentHash"` Amount uint64 `json:"amount"` - FeesPaid uint64 `json:"fees_paid"` - CreatedAt string `json:"created_at"` - SettledAt *string `json:"settled_at"` - AppId *uint `json:"app_id"` - Metadata interface{} `json:"metadata,omitempty"` + FeesPaid uint64 `json:"feesPaid"` + CreatedAt string `json:"createdAt"` + SettledAt *string `json:"settledAt"` + AppId *uint `json:"appId"` + Metadata Metadata `json:"metadata,omitempty"` + Boostagram *Boostagram `json:"boostagram,omitempty"` +} + +type Metadata = map[string]interface{} + +type Boostagram struct { + AppName string `json:"appName"` + Name string `json:"name"` + Podcast string `json:"podcast"` + URL string `json:"url"` + Episode string `json:"episode,omitempty"` + FeedId string `json:"feedId,omitempty"` + ItemId string `json:"itemId,omitempty"` + Timestamp int64 `json:"ts,omitempty"` + Message string `json:"message,omitempty"` + SenderId string `json:"senderId"` + SenderName string `json:"senderName"` + Time string `json:"time"` + Action string `json:"action"` + ValueMsatTotal int64 `json:"valueMsatTotal"` } // debug api diff --git a/api/transactions.go b/api/transactions.go index 8582c36a..49900f1f 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -73,17 +73,30 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction { preimage = transaction.Preimage } - var metadata interface{} - if transaction.Metadata != "" { - jsonErr := json.Unmarshal([]byte(transaction.Metadata), &metadata) + var metadata Metadata + if transaction.Metadata != nil { + jsonErr := json.Unmarshal(transaction.Metadata, &metadata) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ - "id": transaction.ID, - "metadata": transaction.Metadata, + "payment_hash": transaction.PaymentHash, + "metadata": transaction.Metadata, }).Error("Failed to deserialize transaction metadata") } } + var boostagram *Boostagram + if transaction.Boostagram != nil { + var txBoostagram transactions.Boostagram + jsonErr := json.Unmarshal(transaction.Boostagram, &txBoostagram) + if jsonErr != nil { + logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ + "payment_hash": transaction.PaymentHash, + "boostagram": transaction.Boostagram, + }).Error("Failed to deserialize transaction boostagram info") + } + boostagram = toApiBoostagram(&txBoostagram) + } + return &Transaction{ Type: transaction.Type, Invoice: transaction.PaymentRequest, @@ -97,5 +110,25 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction { CreatedAt: createdAt, SettledAt: settledAt, Metadata: metadata, + Boostagram: boostagram, + } +} + +func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram { + return &Boostagram{ + AppName: boostagram.AppName, + Name: boostagram.Name, + Podcast: boostagram.Podcast, + URL: boostagram.URL, + Episode: boostagram.Episode, + FeedId: boostagram.FeedId, + ItemId: boostagram.ItemId, + Timestamp: boostagram.Timestamp, + Message: boostagram.Message, + SenderId: boostagram.SenderId, + SenderName: boostagram.SenderName, + Time: boostagram.Time, + Action: boostagram.Action, + ValueMsatTotal: boostagram.ValueMsatTotal, } } diff --git a/db/migrations/202408061737_add_boostagrams_and_use_json.go b/db/migrations/202408061737_add_boostagrams_and_use_json.go new file mode 100644 index 00000000..ab7f48c7 --- /dev/null +++ b/db/migrations/202408061737_add_boostagrams_and_use_json.go @@ -0,0 +1,29 @@ +package migrations + +import ( + _ "embed" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +// This migration adds boostagram column to transactions +var _202408061737_add_boostagrams_and_use_json = &gormigrate.Migration{ + ID: "202408061737_add_boostagrams_and_use_json", + Migrate: func(db *gorm.DB) error { + err := db.Transaction(func(tx *gorm.DB) error { + return tx.Exec(` + ALTER TABLE transactions ADD COLUMN boostagram JSON; + ALTER TABLE transactions ADD COLUMN metadata_temp JSON; + UPDATE transactions SET metadata_temp = json(metadata) where metadata != ""; + ALTER TABLE transactions DROP COLUMN metadata; + ALTER TABLE transactions RENAME COLUMN metadata_temp TO metadata; + `).Error + }) + + return err + }, + Rollback: func(tx *gorm.DB) error { + return nil + }, +} diff --git a/db/migrations/migrate.go b/db/migrations/migrate.go index 104ddf24..7ef37acf 100644 --- a/db/migrations/migrate.go +++ b/db/migrations/migrate.go @@ -19,6 +19,7 @@ func Migrate(gormDB *gorm.DB) error { _202407151352_autoincrement, _202407201604_transactions_indexes, _202407262257_remove_invalid_scopes, + _202408061737_add_boostagrams_and_use_json, }) return m.Migrate() diff --git a/db/models.go b/db/models.go index f3419b8a..2ea1cf40 100644 --- a/db/models.go +++ b/db/models.go @@ -1,6 +1,10 @@ package db -import "time" +import ( + "time" + + "gorm.io/datatypes" +) type UserConfig struct { ID uint @@ -75,8 +79,9 @@ type Transaction struct { ExpiresAt *time.Time UpdatedAt time.Time SettledAt *time.Time - Metadata string + Metadata datatypes.JSON SelfPayment bool + Boostagram datatypes.JSON } type DBService interface { diff --git a/frontend/src/components/PodcastingInfo.tsx b/frontend/src/components/PodcastingInfo.tsx new file mode 100644 index 00000000..7f207c9a --- /dev/null +++ b/frontend/src/components/PodcastingInfo.tsx @@ -0,0 +1,63 @@ +import { Boostagram } from "src/types"; + +function PodcastingInfo({ boost }: { boost: Boostagram }) { + return ( + <> + {boost.message && ( +
+

Message

+

{boost.message}

+
+ )} + {boost.podcast && ( +
+

Podcast

+

{boost.podcast}

+
+ )} + {boost.episode && ( +
+

Episode

+

{boost.episode}

+
+ )} + {boost.action && ( +
+

Action

+

{boost.action}

+
+ )} + {boost.ts && ( +
+

Timestamp

+

{boost.ts}

+
+ )} + {boost.valueMsatTotal && ( +
+

Total amount

+

+ {new Intl.NumberFormat(undefined, {}).format( + Math.floor(boost.valueMsatTotal / 1000) + )}{" "} + {Math.floor(boost.valueMsatTotal / 1000) == 1 ? "sat" : "sats"} +

+
+ )} + {boost.senderName && ( +
+

Sender

+

{boost.senderName}

+
+ )} + {boost.appName && ( +
+

App

+

{boost.appName}

+
+ )} + + ); +} + +export default PodcastingInfo; diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx index d0e5efe9..c071af1a 100644 --- a/frontend/src/components/TransactionItem.tsx +++ b/frontend/src/components/TransactionItem.tsx @@ -10,6 +10,7 @@ import { } from "lucide-react"; import React from "react"; import AppAvatar from "src/components/AppAvatar"; +import PodcastingInfo from "src/components/PodcastingInfo"; import { Credenza, CredenzaBody, @@ -38,7 +39,7 @@ function TransactionItem({ tx }: Props) { const [showDetails, setShowDetails] = React.useState(false); const type = tx.type; const Icon = tx.type == "outgoing" ? ArrowUpIcon : ArrowDownIcon; - const app = tx.app_id && apps?.find((app) => app.id === tx.app_id); + const app = tx.appId && apps?.find((app) => app.id === tx.appId); const copy = (text: string) => { copyToClipboard(text); @@ -92,7 +93,7 @@ function TransactionItem({ tx }: Props) { {app ? app.name : type == "incoming" ? "Received" : "Sent"}

- {dayjs(tx.settled_at).fromNow()} + {dayjs(tx.settledAt).fromNow()}

@@ -166,7 +167,7 @@ function TransactionItem({ tx }: Props) {

Date & Time

- {dayjs(tx.settled_at) + {dayjs(tx.settledAt) .tz(dayjs.tz.guess()) .format("D MMMM YYYY, HH:mm")}

@@ -176,9 +177,9 @@ function TransactionItem({ tx }: Props) {

Fee

{new Intl.NumberFormat(undefined, {}).format( - Math.floor(tx.fees_paid / 1000) + Math.floor(tx.feesPaid / 1000) )}{" "} - {Math.floor(tx.fees_paid / 1000) == 1 ? "sat" : "sats"} + {Math.floor(tx.feesPaid / 1000) == 1 ? "sat" : "sats"}

)} @@ -191,52 +192,55 @@ function TransactionItem({ tx }: Props) { )} - -
setShowDetails(!showDetails)} - > - Details - {showDetails ? ( - - ) : ( - - )} -
- {showDetails && ( - <> -
-

Preimage

-
-

- {tx.preimage} -

- { - if (tx.preimage) { - copy(tx.preimage); - } - }} - /> + +
+
setShowDetails(!showDetails)} + > + Details + {showDetails ? ( + + ) : ( + + )} +
+ {showDetails && ( + <> + {tx.boostagram && } +
+

Preimage

+
+

+ {tx.preimage} +

+ { + if (tx.preimage) { + copy(tx.preimage); + } + }} + /> +
-
-
-

Hash

-
-

- {tx.payment_hash} -

- { - copy(tx.payment_hash); - }} - /> +
+

Hash

+
+

+ {tx.paymentHash} +

+ { + copy(tx.paymentHash); + }} + /> +
-
- - )} + + )} +
diff --git a/frontend/src/components/TransactionsList.tsx b/frontend/src/components/TransactionsList.tsx index 6e360dd3..816f0dee 100644 --- a/frontend/src/components/TransactionsList.tsx +++ b/frontend/src/components/TransactionsList.tsx @@ -24,7 +24,7 @@ function TransactionsList() { ) : ( <> {transactions?.map((tx) => { - return ; + return ; })} )} diff --git a/frontend/src/screens/wallet/Receive.tsx b/frontend/src/screens/wallet/Receive.tsx index 92547f3a..38393c69 100644 --- a/frontend/src/screens/wallet/Receive.tsx +++ b/frontend/src/screens/wallet/Receive.tsx @@ -43,12 +43,12 @@ export default function Receive() { ); const [paymentDone, setPaymentDone] = React.useState(false); const { data: invoiceData } = useTransaction( - transaction ? transaction.payment_hash : "", + transaction ? transaction.paymentHash : "", true ); React.useEffect(() => { - if (invoiceData?.settled_at) { + if (invoiceData?.settledAt) { setPaymentDone(true); popConfetti(); toast({ diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1ae9851e..7e13c01d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -370,17 +370,35 @@ export type BalancesResponse = { export type Transaction = { type: "incoming" | "outgoing"; - app_id: number | undefined; + appId: number | undefined; invoice: string; description: string; - description_hash: string; + descriptionHash: string; preimage: string | undefined; - payment_hash: string; + paymentHash: string; amount: number; - fees_paid: number; - created_at: string; - settled_at: string | undefined; - metadata: unknown; + feesPaid: number; + createdAt: string; + settledAt: string | undefined; + metadata?: Record; + boostagram?: Boostagram; +}; + +export type Boostagram = { + appName: string; + name: string; + podcast: string; + url: string; + episode?: string; + feedId?: string; + itemId?: string; + ts?: number; + message?: string; + senderId: string; + senderName: string; + time: string; + action: "boost"; + valueMsatTotal: number; }; export type NewChannelOrderStatus = "pay" | "paid" | "success" | "opening"; diff --git a/go.mod b/go.mod index a1e047df..79485257 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.2.1 // indirect github.com/DataDog/datadog-go/v5 v5.3.0 // indirect @@ -70,6 +71,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.1 // indirect @@ -100,7 +102,7 @@ require ( github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect @@ -212,6 +214,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.6 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.49.3 // indirect modernc.org/mathutil v1.6.0 // indirect @@ -234,6 +237,7 @@ require ( github.com/lightningnetwork/lnd v0.17.4-beta github.com/sirupsen/logrus v1.9.3 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + gorm.io/datatypes v1.2.1 ) // See https://github.com/lightningnetwork/lnd/blob/v0.17.4-beta/go.mod#L12C58-L12C70 diff --git a/go.sum b/go.sum index 8801f5ba..bd703ef6 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -209,8 +211,9 @@ github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6Ex github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -233,6 +236,10 @@ github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5 github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -331,8 +338,8 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -345,9 +352,14 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -468,6 +480,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU= +github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -919,6 +933,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0= +gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= +gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk= +gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/lnclient/models.go b/lnclient/models.go index 55a6e459..36d2ee99 100644 --- a/lnclient/models.go +++ b/lnclient/models.go @@ -12,6 +12,8 @@ type TLVRecord struct { Value string `json:"value"` } +type Metadata = map[string]interface{} + type NodeInfo struct { Alias string Color string @@ -34,7 +36,7 @@ type Transaction struct { CreatedAt int64 ExpiresAt *int64 SettledAt *int64 - Metadata interface{} + Metadata Metadata } type NodeConnectionInfo struct { diff --git a/nip47/controllers/make_invoice_controller.go b/nip47/controllers/make_invoice_controller.go index 582ac0ee..98b767a0 100644 --- a/nip47/controllers/make_invoice_controller.go +++ b/nip47/controllers/make_invoice_controller.go @@ -10,11 +10,11 @@ import ( ) type makeInvoiceParams struct { - Amount int64 `json:"amount"` - Description string `json:"description"` - DescriptionHash string `json:"description_hash"` - Expiry int64 `json:"expiry"` - Metadata interface{} `json:"metadata,omitempty"` + Amount int64 `json:"amount"` + Description string `json:"description"` + DescriptionHash string `json:"description_hash"` + Expiry int64 `json:"expiry"` + Metadata map[string]interface{} `json:"metadata,omitempty"` } type makeInvoiceResponse struct { models.Transaction diff --git a/nip47/models/transactions.go b/nip47/models/transactions.go index 2771b18b..09c77c89 100644 --- a/nip47/models/transactions.go +++ b/nip47/models/transactions.go @@ -23,13 +23,13 @@ func ToNip47Transaction(transaction *transactions.Transaction) *Transaction { preimage = *transaction.Preimage } - var metadata interface{} - if transaction.Metadata != "" { - jsonErr := json.Unmarshal([]byte(transaction.Metadata), &metadata) + var metadata map[string]interface{} + if transaction.Metadata != nil { + jsonErr := json.Unmarshal(transaction.Metadata, &metadata) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ - "id": transaction.ID, - "metadata": transaction.Metadata, + "payment_hash": transaction.PaymentHash, + "metadata": transaction.Metadata, }).Error("Failed to deserialize transaction metadata") } } diff --git a/transactions/keysend_test.go b/transactions/keysend_test.go index 8eac2773..81cccf37 100644 --- a/transactions/keysend_test.go +++ b/transactions/keysend_test.go @@ -2,6 +2,7 @@ package transactions import ( "context" + "encoding/json" "testing" "github.com/getAlby/hub/constants" @@ -19,10 +20,15 @@ func TestSendKeysend(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, nil, nil) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, nil, nil) + assert.NoError(t, err) + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + + assert.Equal(t, "fake destination", metadata["destination"]) + assert.Nil(t, metadata["tlv_records"]) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) @@ -39,10 +45,15 @@ func TestSendKeysend_CustomPreimage(t *testing.T) { customPreimage := "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b" transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, customPreimage, svc.LNClient, nil, nil) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, customPreimage, svc.LNClient, nil, nil) + assert.NoError(t, err) + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + + assert.Equal(t, "fake destination", metadata["destination"]) + assert.Nil(t, metadata["tlv_records"]) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) @@ -66,7 +77,7 @@ func TestSendKeysend_App_NoPermission(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.Error(t, err) assert.Equal(t, "app does not have pay_invoice scope", err.Error()) @@ -96,10 +107,15 @@ func TestSendKeysend_App_WithPermission(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + assert.NoError(t, err) + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + + assert.Equal(t, "fake destination", metadata["destination"]) + assert.Nil(t, metadata["tlv_records"]) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) @@ -134,7 +150,7 @@ func TestSendKeysend_App_BudgetExceeded(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.ErrorIs(t, err, NewQuotaExceededError()) assert.Nil(t, transaction) @@ -163,10 +179,15 @@ func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) { assert.NoError(t, err) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + assert.NoError(t, err) + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + + assert.Equal(t, "fake destination", metadata["destination"]) + assert.Nil(t, metadata["tlv_records"]) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) @@ -209,7 +230,7 @@ func TestSendKeysend_App_BalanceExceeded(t *testing.T) { }) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) assert.ErrorIs(t, err, NewInsufficientBalanceError()) assert.Nil(t, transaction) @@ -247,10 +268,15 @@ func TestSendKeysend_App_BalanceSufficient(t *testing.T) { }) transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{}, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID) + assert.NoError(t, err) + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[]}`, transaction.Metadata) + + assert.Equal(t, "fake destination", metadata["destination"]) + assert.Nil(t, metadata["tlv_records"]) assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) @@ -272,12 +298,35 @@ func TestSendKeysend_TLVs(t *testing.T) { transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{ { Type: 7629169, - Value: "48656C6C6F2C20776F726C64", + Value: "7b22616374696f6e223a22626f6f7374222c2276616c75655f6d736174223a313030302c2276616c75655f6d7361745f746f74616c223a313030302c226170705f6e616d65223a22e29aa1205765624c4e2044656d6f222c226170705f76657273696f6e223a22312e30222c22666565644944223a2268747470733a2f2f66656564732e706f6463617374696e6465782e6f72672f706332302e786d6c222c22706f6463617374223a22506f6463617374696e6720322e30222c22657069736f6465223a22457069736f6465203130343a2041204e65772044756d70222c227473223a32312c226e616d65223a22e29aa1205765624c4e2044656d6f222c2273656e6465725f6e616d65223a225361746f736869204e616b616d6f746f222c226d657373616765223a22476f20706f6463617374696e6721227d", }, }, "", svc.LNClient, nil, nil) + assert.NoError(t, err) + + var metadata lnclient.Metadata + err = json.Unmarshal(transaction.Metadata, &metadata) + assert.NoError(t, err) + assert.Equal(t, "fake destination", metadata["destination"]) + assert.NotNil(t, metadata["tlv_records"]) + + var boostagram Boostagram + err = json.Unmarshal(transaction.Boostagram, &boostagram) assert.NoError(t, err) - assert.Equal(t, `{"destination":"fake destination","tlv_records":[{"type":7629169,"value":"48656C6C6F2C20776F726C64"}]}`, transaction.Metadata) + + assert.Equal(t, "⚡ WebLN Demo", boostagram.AppName) + assert.Equal(t, "⚡ WebLN Demo", boostagram.Name) + assert.Equal(t, "Podcasting 2.0", boostagram.Podcast) + assert.Equal(t, "Episode 104: A New Dump", boostagram.Episode) + assert.Equal(t, "https://feeds.podcastindex.org/pc20.xml", boostagram.FeedId) + assert.Equal(t, int64(21), boostagram.Timestamp) + assert.Equal(t, "Go podcasting!", boostagram.Message) + assert.Equal(t, "Satoshi Nakamoto", boostagram.SenderName) + assert.Equal(t, "boost", boostagram.Action) + assert.Equal(t, int64(1000), boostagram.ValueMsatTotal) + + assert.Equal(t, "Go podcasting!", transaction.Description) + assert.Equal(t, uint64(1000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) diff --git a/transactions/make_invoice_test.go b/transactions/make_invoice_test.go index d08331aa..d14b4968 100644 --- a/transactions/make_invoice_test.go +++ b/transactions/make_invoice_test.go @@ -2,6 +2,7 @@ package transactions import ( "context" + "encoding/json" "strings" "testing" @@ -18,16 +19,21 @@ func TestMakeInvoice_NoApp(t *testing.T) { svc, err := tests.CreateTestService() assert.NoError(t, err) - metadata := strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-2) // json encoding adds 2 characters + txMetadata := make(map[string]interface{}) + txMetadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-16) // json encoding adds 16 characters - {"randomkey":""} transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, metadata, svc.LNClient, nil, nil) + transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, txMetadata, svc.LNClient, nil, nil) + assert.NoError(t, err) + var metadata map[string]interface{} + err = json.Unmarshal(transaction.Metadata, &metadata) assert.NoError(t, err) + assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State) assert.Equal(t, tests.MockLNClientTransaction.Preimage, *transaction.Preimage) - assert.Equal(t, `"`+metadata+`"`, transaction.Metadata) // json-encoded + assert.Equal(t, txMetadata["randomkey"], metadata["randomkey"]) } func TestMakeInvoice_MetadataTooLarge(t *testing.T) { @@ -36,7 +42,9 @@ func TestMakeInvoice_MetadataTooLarge(t *testing.T) { defer tests.RemoveTestService() svc, err := tests.CreateTestService() assert.NoError(t, err) - metadata := strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-1) // json encoding adds 2 characters + + metadata := make(map[string]interface{}) + metadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-15) // json encoding adds 16 characters transactionsService := NewTransactionsService(svc.DB) transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, metadata, svc.LNClient, nil, nil) diff --git a/transactions/notifications_test.go b/transactions/notifications_test.go index d2be68ff..2851bffc 100644 --- a/transactions/notifications_test.go +++ b/transactions/notifications_test.go @@ -2,11 +2,13 @@ package transactions import ( "context" + "encoding/json" "testing" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" + "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/tests" "github.com/stretchr/testify/assert" ) @@ -74,6 +76,72 @@ func TestNotifications_ReceivedUnknownPayment(t *testing.T) { assert.Equal(t, int64(1), result.RowsAffected) } +func TestNotifications_ReceivedKeysend(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + + metadata := map[string]interface{}{} + + metadata["tlv_records"] = []lnclient.TLVRecord{ + { + Type: 7629169, + Value: "7b22616374696f6e223a22626f6f7374222c2276616c75655f6d736174223a313030302c2276616c75655f6d7361745f746f74616c223a313030302c226170705f6e616d65223a22e29aa1205765624c4e2044656d6f222c226170705f76657273696f6e223a22312e30222c22666565644944223a2268747470733a2f2f66656564732e706f6463617374696e6465782e6f72672f706332302e786d6c222c22706f6463617374223a22506f6463617374696e6720322e30222c22657069736f6465223a22457069736f6465203130343a2041204e65772044756d70222c227473223a32312c226e616d65223a22e29aa1205765624c4e2044656d6f222c2273656e6465725f6e616d65223a225361746f736869204e616b616d6f746f222c226d657373616765223a22476f20706f6463617374696e6721227d", + }, + } + + transaction := &lnclient.Transaction{ + Type: "incoming", + Invoice: tests.MockInvoice, + Description: "", + DescriptionHash: "", + Preimage: tests.MockLNClientTransaction.Preimage, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Amount: 2000, + FeesPaid: 75, + SettledAt: &tests.MockTimeUnix, + Metadata: metadata, + } + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_received", + Properties: transaction, + }, map[string]interface{}{}) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(2000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage) + assert.Zero(t, incomingTransaction.FeeReserveMsat) + + var boostagram Boostagram + err = json.Unmarshal(incomingTransaction.Boostagram, &boostagram) + assert.NoError(t, err) + + assert.Equal(t, "⚡ WebLN Demo", boostagram.AppName) + assert.Equal(t, "⚡ WebLN Demo", boostagram.Name) + assert.Equal(t, "Podcasting 2.0", boostagram.Podcast) + assert.Equal(t, "Episode 104: A New Dump", boostagram.Episode) + assert.Equal(t, "https://feeds.podcastindex.org/pc20.xml", boostagram.FeedId) + assert.Equal(t, int64(21), boostagram.Timestamp) + assert.Equal(t, "Go podcasting!", boostagram.Message) + assert.Equal(t, "Satoshi Nakamoto", boostagram.SenderName) + assert.Equal(t, "boost", boostagram.Action) + assert.Equal(t, int64(1000), boostagram.ValueMsatTotal) + + assert.Equal(t, "Go podcasting!", incomingTransaction.Description) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} + func TestNotifications_SentKnownPayment(t *testing.T) { ctx := context.TODO() @@ -130,15 +198,8 @@ func TestNotifications_SentUnknownPayment(t *testing.T) { transactionType := constants.TRANSACTION_TYPE_OUTGOING outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) - assert.NoError(t, err) - assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), outgoingTransaction.AmountMsat) - assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransaction.State) - assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage) - assert.Zero(t, outgoingTransaction.FeeReserveMsat) - - transactions = []db.Transaction{} - result = svc.DB.Find(&transactions) - assert.Equal(t, int64(1), result.RowsAffected) + assert.Nil(t, outgoingTransaction) + assert.ErrorIs(t, err, NewNotFoundError()) } func TestNotifications_FailedKnownPayment(t *testing.T) { diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 5fbcccda..b48f22c7 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -21,6 +21,7 @@ import ( "github.com/getAlby/hub/logger" decodepay "github.com/nbd-wtf/ln-decodepay" "github.com/sirupsen/logrus" + "gorm.io/datatypes" "gorm.io/gorm" ) @@ -30,15 +31,37 @@ type transactionsService struct { type TransactionsService interface { events.EventSubscriber - MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) + MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) } +const ( + BoostagramTlvType = 7629169 + WhatsatTlvType = 34349334 +) + type Transaction = db.Transaction +type Boostagram struct { + AppName string `json:"app_name"` + Name string `json:"name"` + Podcast string `json:"podcast"` + URL string `json:"url"` + Episode string `json:"episode,omitempty"` + FeedId string `json:"feedID,omitempty"` + ItemId string `json:"itemID,omitempty"` + Timestamp int64 `json:"ts,omitempty"` + Message string `json:"message,omitempty"` + SenderId string `json:"sender_id"` + SenderName string `json:"sender_name"` + Time string `json:"time"` + Action string `json:"action"` + ValueMsatTotal int64 `json:"value_msat_total"` +} + type notFoundError struct { } @@ -78,10 +101,11 @@ func NewTransactionsService(db *gorm.DB) *transactionsService { } } -func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { - var encodedMetadata string +func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { + var metadataBytes []byte if metadata != nil { - metadataBytes, err := json.Marshal(metadata) + var err error + metadataBytes, err = json.Marshal(metadata) if err != nil { logger.Logger.WithError(err).Error("Failed to serialize metadata") return nil, err @@ -89,7 +113,6 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, d if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH { return nil, fmt.Errorf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes)) } - encodedMetadata = string(metadataBytes) } lnClientTransaction, err := lnClient.MakeInvoice(ctx, amount, description, descriptionHash, expiry) @@ -121,7 +144,7 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, d PaymentHash: lnClientTransaction.PaymentHash, ExpiresAt: expiresAt, Preimage: preimage, - Metadata: encodedMetadata, + Metadata: datatypes.JSON(metadataBytes), } err = svc.db.Create(&dbTransaction).Error if err != nil { @@ -263,13 +286,14 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, metadata := map[string]interface{}{} metadata["destination"] = destination + metadata["tlv_records"] = customRecords metadataBytes, err := json.Marshal(metadata) - if err != nil { - logger.Logger.WithError(err).Error("Failed to marshal metadata") + logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") return nil, err } + boostagramBytes := svc.getBoostagramFromCustomRecords(customRecords) var dbTransaction db.Transaction @@ -281,12 +305,14 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, dbTransaction = db.Transaction{ AppId: appId, + Description: svc.getDescriptionFromCustomRecords(customRecords), RequestEventId: requestEventId, Type: constants.TRANSACTION_TYPE_OUTGOING, State: constants.TRANSACTION_STATE_PENDING, FeeReserveMsat: svc.calculateFeeReserveMsat(uint64(amount)), AmountMsat: amount, - Metadata: string(metadataBytes), + Metadata: datatypes.JSON(metadataBytes), + Boostagram: datatypes.JSON(boostagramBytes), PaymentHash: paymentHash, Preimage: &preimage, } @@ -532,15 +558,25 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. }) if result.RowsAffected == 0 { - // Note: brand new payments cannot be associated with an app - var metadata string + // TODO: support customkey/customvalue for boostagrams received to isolated apps + description := lnClientTransaction.Description + var metadataBytes []byte + var boostagramBytes []byte if lnClientTransaction.Metadata != nil { - metadataBytes, err := json.Marshal(lnClientTransaction.Metadata) + var err error + metadataBytes, err = json.Marshal(lnClientTransaction.Metadata) if err != nil { logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") return err } - metadata = string(metadataBytes) + + var customRecords []lnclient.TLVRecord + customRecords, _ = lnClientTransaction.Metadata["tlv_records"].([]lnclient.TLVRecord) + boostagramBytes = svc.getBoostagramFromCustomRecords(customRecords) + extractedDescription := svc.getDescriptionFromCustomRecords(customRecords) + if extractedDescription != "" { + description = extractedDescription + } } var expiresAt *time.Time if lnClientTransaction.ExpiresAt != nil { @@ -552,10 +588,11 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. AmountMsat: uint64(lnClientTransaction.Amount), PaymentRequest: lnClientTransaction.Invoice, PaymentHash: lnClientTransaction.PaymentHash, - Description: lnClientTransaction.Description, + Description: description, DescriptionHash: lnClientTransaction.DescriptionHash, ExpiresAt: expiresAt, - Metadata: metadata, + Metadata: datatypes.JSON(metadataBytes), + Boostagram: datatypes.JSON(boostagramBytes), } err := tx.Create(&dbTransaction).Error if err != nil { @@ -606,38 +643,10 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. }) if result.RowsAffected == 0 { - // Note: brand new payments cannot be associated with an app - var metadata string - if lnClientTransaction.Metadata != nil { - metadataBytes, err := json.Marshal(lnClientTransaction.Metadata) - if err != nil { - logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") - return err - } - metadata = string(metadataBytes) - } - var expiresAt *time.Time - if lnClientTransaction.ExpiresAt != nil { - expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) - expiresAt = &expiresAtValue - } - dbTransaction = db.Transaction{ - Type: constants.TRANSACTION_TYPE_OUTGOING, - AmountMsat: uint64(lnClientTransaction.Amount), - PaymentRequest: lnClientTransaction.Invoice, - PaymentHash: lnClientTransaction.PaymentHash, - Description: lnClientTransaction.Description, - DescriptionHash: lnClientTransaction.DescriptionHash, - ExpiresAt: expiresAt, - Metadata: metadata, - } - err := tx.Create(&dbTransaction).Error - if err != nil { - logger.Logger.WithFields(logrus.Fields{ - "payment_hash": lnClientTransaction.PaymentHash, - }).WithError(err).Error("Failed to create transaction") - return err - } + // Note: payments made from outside cannot be associated with an app + // for now this is disabled as it only applies to LND, and we do not import LND transactions either. + logger.Logger.WithField("payment_hash", lnClientTransaction.PaymentHash).Error("payment not found") + return NewNotFoundError() } settledAt := time.Now() @@ -784,3 +793,45 @@ func makePreimageHex() ([]byte, error) { } return bytes, nil } + +func (svc *transactionsService) getBoostagramFromCustomRecords(customRecords []lnclient.TLVRecord) []byte { + for _, record := range customRecords { + if record.Type == BoostagramTlvType { + bytes, err := hex.DecodeString(record.Value) + if err != nil { + return nil + } + return bytes + } + } + + return nil +} + +func (svc *transactionsService) getDescriptionFromCustomRecords(customRecords []lnclient.TLVRecord) string { + var description string + + for _, record := range customRecords { + switch record.Type { + case BoostagramTlvType: + bytes, err := hex.DecodeString(record.Value) + if err != nil { + continue + } + var boostagram Boostagram + if err := json.Unmarshal(bytes, &boostagram); err != nil { + continue + } + return boostagram.Message + + // TODO: consider adding support for this in LDK + case WhatsatTlvType: + bytes, err := hex.DecodeString(record.Value) + if err == nil { + description = string(bytes) + } + } + } + + return description +}