feat_: Unfurl transaction deep link
This commit is contained in:
parent
4c889399eb
commit
0c7f9f34a0
|
@ -54,6 +54,15 @@ type StatusCommunityLinkPreview struct {
|
|||
Banner LinkPreviewThumbnail `json:"banner,omitempty"`
|
||||
}
|
||||
|
||||
type StatusTransactionLinkPreview struct {
|
||||
TxType int `json:"txType"`
|
||||
Amount string `json:"amount"`
|
||||
Asset string `json:"asset"`
|
||||
ToAsset string `json:"toAsset"`
|
||||
Address string `json:"address"`
|
||||
ChainID int `json:"chainId"`
|
||||
}
|
||||
|
||||
type StatusCommunityChannelLinkPreview struct {
|
||||
ChannelUUID string `json:"channelUuid"`
|
||||
Emoji string `json:"emoji"`
|
||||
|
@ -64,10 +73,11 @@ type StatusCommunityChannelLinkPreview struct {
|
|||
}
|
||||
|
||||
type StatusLinkPreview struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Contact *StatusContactLinkPreview `json:"contact,omitempty"`
|
||||
Community *StatusCommunityLinkPreview `json:"community,omitempty"`
|
||||
Channel *StatusCommunityChannelLinkPreview `json:"channel,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Contact *StatusContactLinkPreview `json:"contact,omitempty"`
|
||||
Community *StatusCommunityLinkPreview `json:"community,omitempty"`
|
||||
Channel *StatusCommunityChannelLinkPreview `json:"channel,omitempty"`
|
||||
Transaction *StatusTransactionLinkPreview `json:"transaction,omitempty"`
|
||||
}
|
||||
|
||||
func (thumbnail *LinkPreviewThumbnail) IsEmpty() bool {
|
||||
|
@ -161,17 +171,23 @@ func (preview *StatusLinkPreview) validateForProto() error {
|
|||
}
|
||||
|
||||
// At least and only one of Contact/Community/Channel should be present in the preview
|
||||
if preview.Contact != nil && preview.Community != nil {
|
||||
return fmt.Errorf("both contact and community are set at the same time")
|
||||
var linkTypes []string
|
||||
if preview.Contact != nil {
|
||||
linkTypes = append(linkTypes, "Contact")
|
||||
}
|
||||
if preview.Community != nil && preview.Channel != nil {
|
||||
return fmt.Errorf("both community and channel are set at the same time")
|
||||
if preview.Community != nil {
|
||||
linkTypes = append(linkTypes, "Community")
|
||||
}
|
||||
if preview.Channel != nil && preview.Contact != nil {
|
||||
return fmt.Errorf("both contact and channel are set at the same time")
|
||||
if preview.Channel != nil {
|
||||
linkTypes = append(linkTypes, "Channel")
|
||||
}
|
||||
if preview.Contact == nil && preview.Community == nil && preview.Channel == nil {
|
||||
return fmt.Errorf("none of contact/community/channel are set")
|
||||
if preview.Transaction != nil {
|
||||
linkTypes = append(linkTypes, "Transaction")
|
||||
}
|
||||
if len(linkTypes) > 1 {
|
||||
return fmt.Errorf("multiple components set at the same time: %v", linkTypes)
|
||||
} else if len(linkTypes) == 0 {
|
||||
return fmt.Errorf("none of contact/community/channel/transaction are set")
|
||||
}
|
||||
|
||||
if preview.Contact != nil {
|
||||
|
@ -200,6 +216,14 @@ func (preview *StatusLinkPreview) validateForProto() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if preview.Transaction != nil {
|
||||
if preview.Transaction.Asset == "" && preview.Transaction.Amount == "" && preview.Transaction.Address == "" && preview.Transaction.ToAsset == "" {
|
||||
return fmt.Errorf("transaction fields are empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -444,6 +468,19 @@ func (m *Message) ConvertStatusLinkPreviewsToProto() (*protobuf.UnfurledStatusLi
|
|||
|
||||
}
|
||||
|
||||
if preview.Transaction != nil {
|
||||
ul.Payload = &protobuf.UnfurledStatusLink_Transaction{
|
||||
Transaction: &protobuf.UnfurledStatusTransactionLink{
|
||||
TxType: uint32(preview.Transaction.TxType),
|
||||
Amount: preview.Transaction.Amount,
|
||||
Asset: preview.Transaction.Asset,
|
||||
ToAsset: preview.Transaction.ToAsset,
|
||||
Address: preview.Transaction.Address,
|
||||
ChainId: uint32(preview.Transaction.ChainID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unfurledLinks = append(unfurledLinks, ul)
|
||||
}
|
||||
|
||||
|
@ -508,6 +545,17 @@ func (m *Message) ConvertFromProtoToStatusLinkPreviews(makeMediaServerURL func(m
|
|||
}
|
||||
}
|
||||
|
||||
if c := link.GetTransaction(); c != nil {
|
||||
lp.Transaction = &StatusTransactionLinkPreview{
|
||||
TxType: int(c.TxType),
|
||||
Amount: c.Amount,
|
||||
Asset: c.Asset,
|
||||
ToAsset: c.ToAsset,
|
||||
Address: c.Address,
|
||||
ChainID: int(c.ChainId),
|
||||
}
|
||||
}
|
||||
|
||||
previews = append(previews, lp)
|
||||
}
|
||||
|
||||
|
|
|
@ -338,6 +338,15 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
transaction := &StatusTransactionLinkPreview{
|
||||
TxType: 2,
|
||||
Amount: "Amount_22",
|
||||
Asset: "Asset_23",
|
||||
ToAsset: "ToAsset_24",
|
||||
Address: "Address_25",
|
||||
ChainID: 26,
|
||||
}
|
||||
|
||||
message := Message{
|
||||
StatusLinkPreviews: []StatusLinkPreview{
|
||||
{
|
||||
|
@ -352,6 +361,10 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
URL: "https://status.app/cc/",
|
||||
Channel: channel,
|
||||
},
|
||||
{
|
||||
URL: "https://status.app/tx/",
|
||||
Transaction: transaction,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -360,7 +373,7 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
|
||||
unfurledLinks, err := message.ConvertStatusLinkPreviewsToProto()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, unfurledLinks.UnfurledStatusLinks, 3)
|
||||
require.Len(t, unfurledLinks.UnfurledStatusLinks, 4)
|
||||
|
||||
// Contact link
|
||||
|
||||
|
@ -369,6 +382,7 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
require.NotNil(t, l1.GetContact())
|
||||
require.Nil(t, l1.GetCommunity())
|
||||
require.Nil(t, l1.GetChannel())
|
||||
require.Nil(t, l1.GetTransaction())
|
||||
c1 := l1.GetContact()
|
||||
require.Equal(t, compressedContactPublicKey, c1.PublicKey)
|
||||
require.Equal(t, contact.DisplayName, c1.DisplayName)
|
||||
|
@ -385,6 +399,7 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
require.NotNil(t, l2.GetCommunity())
|
||||
require.Nil(t, l2.GetContact())
|
||||
require.Nil(t, l2.GetChannel())
|
||||
require.Nil(t, l2.GetTransaction())
|
||||
c2 := l2.GetCommunity()
|
||||
require.Equal(t, compressedCommunityPublicKey, c2.CommunityId)
|
||||
require.Equal(t, community.DisplayName, c2.DisplayName)
|
||||
|
@ -407,6 +422,7 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
require.NotNil(t, l3.GetChannel())
|
||||
require.Nil(t, l3.GetContact())
|
||||
require.Nil(t, l3.GetCommunity())
|
||||
require.Nil(t, l3.GetTransaction())
|
||||
|
||||
c3 := l3.GetChannel()
|
||||
require.Equal(t, channel.ChannelUUID, c3.ChannelUuid)
|
||||
|
@ -430,6 +446,23 @@ func TestConvertStatusLinkPreviewsToProto(t *testing.T) {
|
|||
require.Equal(t, uint32(channel.Community.Banner.Height), c3.Community.Banner.Height)
|
||||
require.Equal(t, expectedThumbnailPayload, c3.Community.Banner.Payload)
|
||||
|
||||
// Transaction link
|
||||
|
||||
l4 := unfurledLinks.UnfurledStatusLinks[3]
|
||||
require.Equal(t, "https://status.app/tx/", l4.Url)
|
||||
require.NotNil(t, l4.GetTransaction())
|
||||
require.Nil(t, l4.GetContact())
|
||||
require.Nil(t, l4.GetCommunity())
|
||||
require.Nil(t, l4.GetChannel())
|
||||
|
||||
t4 := l4.GetTransaction()
|
||||
require.Equal(t, uint32(transaction.TxType), t4.TxType)
|
||||
require.Equal(t, transaction.Amount, t4.Amount)
|
||||
require.Equal(t, transaction.Asset, t4.Asset)
|
||||
require.Equal(t, transaction.ToAsset, t4.ToAsset)
|
||||
require.Equal(t, transaction.Address, t4.Address)
|
||||
require.Equal(t, uint32(transaction.ChainID), t4.ChainId)
|
||||
|
||||
// Test any invalid link preview causes an early return.
|
||||
invalidContactPreview := contact
|
||||
invalidContactPreview.PublicKey = ""
|
||||
|
@ -516,6 +549,15 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
transaction := &protobuf.UnfurledStatusTransactionLink{
|
||||
TxType: 1,
|
||||
Amount: "100",
|
||||
Asset: "ETH",
|
||||
ToAsset: "DAI",
|
||||
Address: "0x1234567890",
|
||||
ChainId: 11,
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
ID: "42",
|
||||
ChatMessage: &protobuf.ChatMessage{
|
||||
|
@ -539,6 +581,12 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
Channel: channel,
|
||||
},
|
||||
},
|
||||
{
|
||||
Url: "https://status.app/tx/",
|
||||
Payload: &protobuf.UnfurledStatusLink_Transaction{
|
||||
Transaction: transaction,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -549,7 +597,7 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
}
|
||||
|
||||
previews := msg.ConvertFromProtoToStatusLinkPreviews(urlMaker)
|
||||
require.Len(t, previews, 3)
|
||||
require.Len(t, previews, 4)
|
||||
|
||||
// Contact preview
|
||||
|
||||
|
@ -558,6 +606,7 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
require.NotNil(t, p1.Contact)
|
||||
require.Nil(t, p1.Community)
|
||||
require.Nil(t, p1.Channel)
|
||||
require.Nil(t, p1.Transaction)
|
||||
|
||||
c1 := p1.Contact
|
||||
require.NotNil(t, c1)
|
||||
|
@ -577,6 +626,7 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
require.NotNil(t, p2.Community)
|
||||
require.Nil(t, p2.Contact)
|
||||
require.Nil(t, p2.Channel)
|
||||
require.Nil(t, p2.Transaction)
|
||||
|
||||
c2 := p2.Community
|
||||
require.Equal(t, communityID, c2.CommunityID)
|
||||
|
@ -602,6 +652,7 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
require.NotNil(t, p3.Channel)
|
||||
require.Nil(t, p3.Contact)
|
||||
require.Nil(t, p3.Community)
|
||||
require.Nil(t, p3.Transaction)
|
||||
|
||||
c3 := previews[2].Channel
|
||||
require.Equal(t, channel.ChannelUuid, c3.ChannelUUID)
|
||||
|
@ -627,6 +678,20 @@ func TestConvertFromProtoToStatusLinkPreviews(t *testing.T) {
|
|||
require.Equal(t, "", c3.Community.Banner.DataURI)
|
||||
require.Equal(t, "https://localhost:6666/42-https://status.app/cc/-community-channel-banner", c3.Community.Banner.URL)
|
||||
|
||||
p4 := previews[3]
|
||||
require.Equal(t, "https://status.app/tx/", p4.URL)
|
||||
require.NotNil(t, p4.Transaction)
|
||||
require.Nil(t, p4.Contact)
|
||||
require.Nil(t, p4.Community)
|
||||
require.Nil(t, p4.Channel)
|
||||
|
||||
t1 := p4.Transaction
|
||||
require.Equal(t, transaction.TxType, uint32(t1.TxType))
|
||||
require.Equal(t, transaction.Amount, t1.Amount)
|
||||
require.Equal(t, transaction.Asset, t1.Asset)
|
||||
require.Equal(t, transaction.ToAsset, t1.ToAsset)
|
||||
require.Equal(t, transaction.Address, t1.Address)
|
||||
require.Equal(t, transaction.ChainId, uint32(t1.ChainID))
|
||||
}
|
||||
|
||||
func assertMarshalAndUnmarshalJSON[T any](t *testing.T, obj *T, msgAndArgs ...any) {
|
||||
|
|
|
@ -83,6 +83,17 @@ func (u *StatusUnfurler) buildContactData(publicKey string) (*common.StatusConta
|
|||
return c, nil
|
||||
}
|
||||
|
||||
func (u *StatusUnfurler) buildTransactionData(urlData *TransactionURLData) (*common.StatusTransactionLinkPreview, error) {
|
||||
return &common.StatusTransactionLinkPreview{
|
||||
TxType: urlData.TxType,
|
||||
Asset: urlData.Asset,
|
||||
Amount: urlData.Amount,
|
||||
Address: urlData.Address,
|
||||
ChainID: urlData.ChainID,
|
||||
ToAsset: urlData.ToAsset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *StatusUnfurler) buildCommunityData(communityID string, shard *shard.Shard) (*communities.Community, *common.StatusCommunityLinkPreview, error) {
|
||||
// This automatically checks the database
|
||||
community, err := u.m.FetchCommunity(&FetchCommunityRequest{
|
||||
|
@ -172,5 +183,13 @@ func (u *StatusUnfurler) Unfurl() (*common.StatusLinkPreview, error) {
|
|||
return preview, nil
|
||||
}
|
||||
|
||||
if resp.Transaction != nil {
|
||||
preview.Transaction, err = u.buildTransactionData(resp.Transaction)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when building transaction data: %w", err)
|
||||
}
|
||||
return preview, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("shared url does not contain contact, community or channel data")
|
||||
}
|
||||
|
|
|
@ -171,12 +171,22 @@ message UnfurledStatusChannelLink {
|
|||
UnfurledStatusCommunityLink community = 6;
|
||||
}
|
||||
|
||||
message UnfurledStatusTransactionLink {
|
||||
uint32 txType = 1;
|
||||
string address = 2;
|
||||
string amount = 3;
|
||||
string asset = 4;
|
||||
uint32 chainId = 5;
|
||||
string toAsset = 6;
|
||||
}
|
||||
|
||||
message UnfurledStatusLink {
|
||||
string url = 1;
|
||||
oneof payload {
|
||||
UnfurledStatusContactLink contact = 2;
|
||||
UnfurledStatusCommunityLink community = 3;
|
||||
UnfurledStatusChannelLink channel = 4;
|
||||
UnfurledStatusTransactionLink transaction = 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -265,6 +265,15 @@ func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
|
|||
},
|
||||
}
|
||||
|
||||
transaction := &protobuf.UnfurledStatusTransactionLink{
|
||||
TxType: 2,
|
||||
Asset: "Asset_1",
|
||||
Amount: "Amount_1",
|
||||
Address: "Address_1",
|
||||
ToAsset: "ToAsset_1",
|
||||
ChainId: 11,
|
||||
}
|
||||
|
||||
unfurledContact := &protobuf.UnfurledStatusLink{
|
||||
Url: "https://status.app/u/",
|
||||
Payload: &protobuf.UnfurledStatusLink_Contact{
|
||||
|
@ -293,12 +302,20 @@ func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
|
|||
},
|
||||
}
|
||||
|
||||
unfurledTransaction := &protobuf.UnfurledStatusLink{
|
||||
Url: "https://status.app/tx/",
|
||||
Payload: &protobuf.UnfurledStatusLink_Transaction{
|
||||
Transaction: transaction,
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
messageIDContactOnly = "1"
|
||||
messageIDCommunityOnly = "2"
|
||||
messageIDChannelOnly = "3"
|
||||
messageIDAllLinks = "4"
|
||||
messageIDUnsupportedImage = "5"
|
||||
messageIDTransactionOnly = "6"
|
||||
)
|
||||
|
||||
s.saveUserMessage(&common.Message{
|
||||
|
@ -342,6 +359,7 @@ func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
|
|||
unfurledContact,
|
||||
unfurledCommunity,
|
||||
unfurledChannel,
|
||||
unfurledTransaction,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -358,6 +376,17 @@ func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
|
|||
},
|
||||
})
|
||||
|
||||
s.saveUserMessage(&common.Message{
|
||||
ID: messageIDTransactionOnly,
|
||||
ChatMessage: &protobuf.ChatMessage{
|
||||
UnfurledStatusLinks: &protobuf.UnfurledStatusLinks{
|
||||
UnfurledStatusLinks: []*protobuf.UnfurledStatusLink{
|
||||
unfurledTransaction,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
ExpectedHTTPStatusCode int
|
||||
|
@ -538,6 +567,26 @@ func (s *HandlersSuite) TestHandleStatusLinkPreviewThumbnail() {
|
|||
s.Require().Equal("missing query parameter 'image-id'\n", rr.Body.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Test request with missing 'message-id' parameter",
|
||||
Parameters: url.Values{
|
||||
"url": {unfurledTransaction.Url},
|
||||
},
|
||||
ExpectedHTTPStatusCode: http.StatusBadRequest,
|
||||
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
|
||||
s.Require().Equal("missing query parameter 'message-id'\n", rr.Body.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Test request with missing 'url' parameter",
|
||||
Parameters: url.Values{
|
||||
"message-id": {messageIDTransactionOnly},
|
||||
},
|
||||
ExpectedHTTPStatusCode: http.StatusBadRequest,
|
||||
CheckFunc: func(s *HandlersSuite, rr *httptest.ResponseRecorder) {
|
||||
s.Require().Equal("missing query parameter 'url'\n", rr.Body.String())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handler := handleStatusLinkPreviewThumbnail(s.db, s.logger)
|
||||
|
|
Loading…
Reference in New Issue