@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/aws/aws-sdk-go-v2/aws"
1212 "github.com/aws/aws-sdk-go-v2/service/s3"
13+ "github.com/pkg/errors"
1314 "go.uber.org/zap"
1415 "golang.org/x/image/draw"
1516 "google.golang.org/grpc/codes"
@@ -28,6 +29,8 @@ const iconSize = 64
2829var (
2930 jpegMagic = []byte {0xFF , 0xD8 , 0xFF }
3031 pngMagic = []byte {0x89 , 0x50 , 0x4E , 0x47 }
32+
33+ errInvalidIcon = errors .New ("invalid icon" )
3134)
3235
3336func (s * currencyServer ) UpdateIcon (ctx context.Context , req * currencypb.UpdateIconRequest ) (* currencypb.UpdateIconResponse , error ) {
@@ -69,23 +72,52 @@ func (s *currencyServer) UpdateIcon(ctx context.Context, req *currencypb.UpdateI
6972 return & currencypb.UpdateIconResponse {Result : currencypb .UpdateIconResponse_DENIED }, nil
7073 }
7174
75+ processed , ext , contentType , err := processIcon (req .Icon )
76+ if err == errInvalidIcon {
77+ return & currencypb.UpdateIconResponse {Result : currencypb .UpdateIconResponse_INVALID_ICON }, nil
78+ } else if err != nil {
79+ log .With (zap .Error (err )).Warn ("failed to process icon" )
80+ return nil , status .Error (codes .Internal , "" )
81+ }
82+
83+ imageUrl , err := uploadIcon (ctx , s .s3Client , mintAccount , processed , ext , contentType )
84+ if err != nil {
85+ log .With (zap .Error (err )).Warn ("failed to upload icon to s3" )
86+ return nil , status .Error (codes .Internal , "" )
87+ }
88+
89+ metadataRecord .ImageUrl = imageUrl
90+
91+ err = s .data .SaveCurrencyMetadata (ctx , metadataRecord )
92+ if err != nil {
93+ log .With (zap .Error (err )).Warn ("failed to save currency metadata" )
94+ return nil , status .Error (codes .Internal , "" )
95+ }
96+
97+ return & currencypb.UpdateIconResponse {Result : currencypb .UpdateIconResponse_OK }, nil
98+ }
99+
100+ // processIcon validates, decodes, resizes, and re-encodes raw icon data.
101+ // It returns the processed image bytes, the file extension ("jpg" or "png"),
102+ // and the content type. Returns errInvalidIcon if the data is not a valid
103+ // JPEG or PNG image.
104+ func processIcon (data []byte ) ([]byte , string , string , error ) {
72105 var contentType string
73106 var ext string
74- iconData := req .Icon
75107 switch {
76- case len (iconData ) >= len (jpegMagic ) && bytes .Equal (iconData [:len (jpegMagic )], jpegMagic ):
108+ case len (data ) >= len (jpegMagic ) && bytes .Equal (data [:len (jpegMagic )], jpegMagic ):
77109 contentType = "image/jpeg"
78110 ext = "jpg"
79- case len (iconData ) >= len (pngMagic ) && bytes .Equal (iconData [:len (pngMagic )], pngMagic ):
111+ case len (data ) >= len (pngMagic ) && bytes .Equal (data [:len (pngMagic )], pngMagic ):
80112 contentType = "image/png"
81113 ext = "png"
82114 default :
83- return & currencypb. UpdateIconResponse { Result : currencypb . UpdateIconResponse_INVALID_ICON }, nil
115+ return nil , "" , "" , errInvalidIcon
84116 }
85117
86- src , _ , err := image .Decode (bytes .NewReader (iconData ))
118+ src , _ , err := image .Decode (bytes .NewReader (data ))
87119 if err != nil {
88- return & currencypb. UpdateIconResponse { Result : currencypb . UpdateIconResponse_INVALID_ICON }, nil
120+ return nil , "" , "" , errInvalidIcon
89121 }
90122
91123 if bounds := src .Bounds (); bounds .Dx () != iconSize || bounds .Dy () != iconSize {
@@ -102,33 +134,45 @@ func (s *currencyServer) UpdateIcon(ctx context.Context, req *currencypb.UpdateI
102134 err = png .Encode (& encoded , src )
103135 }
104136 if err != nil {
105- log .With (zap .Error (err )).Warn ("failed to encode resized icon" )
106- return nil , status .Error (codes .Internal , "" )
137+ return nil , "" , "" , errors .Wrap (err , "failed to encode icon" )
107138 }
108139
109- mint := mintAccount .PublicKey ().ToBase58 ()
110- key := fmt .Sprintf ("%s/icon.%s" , mint , ext )
111- bucket := config .CurrencyAssetsS3BucketName
140+ return encoded .Bytes (), ext , contentType , nil
141+ }
142+
143+ // uploadIcon uploads processed icon data to S3 and returns the public URL.
144+ func uploadIcon (ctx context.Context , s3Client * s3.Client , mint * common.Account , data []byte , ext string , contentType string ) (string , error ) {
145+ key := iconKey (mint , ext )
112146
113- putReq := s . s3Client .PutObjectRequest (& s3.PutObjectInput {
114- Bucket : aws .String (bucket ),
147+ putReq := s3Client .PutObjectRequest (& s3.PutObjectInput {
148+ Bucket : aws .String (config . CurrencyAssetsS3BucketName ),
115149 Key : aws .String (key ),
116- Body : bytes .NewReader (encoded . Bytes () ),
150+ Body : bytes .NewReader (data ),
117151 ContentType : aws .String (contentType ),
118152 })
119- _ , err = putReq .Send (ctx )
153+ _ , err : = putReq .Send (ctx )
120154 if err != nil {
121- log .With (zap .Error (err )).Warn ("failed to upload icon to s3" )
122- return nil , status .Error (codes .Internal , "" )
155+ return "" , errors .Wrap (err , "failed to upload icon to s3" )
123156 }
124157
125- metadataRecord .ImageUrl = fmt .Sprintf ("%s/%s" , config .CurrencyAssetsBaseUrl , key )
158+ return fmt .Sprintf ("%s/%s" , config .CurrencyAssetsBaseUrl , key ), nil
159+ }
126160
127- err = s .data .SaveCurrencyMetadata (ctx , metadataRecord )
161+ // deleteIcon removes a previously uploaded icon from S3.
162+ func deleteIcon (ctx context.Context , s3Client * s3.Client , mint * common.Account , ext string ) error {
163+ key := iconKey (mint , ext )
164+
165+ deleteReq := s3Client .DeleteObjectRequest (& s3.DeleteObjectInput {
166+ Bucket : aws .String (config .CurrencyAssetsS3BucketName ),
167+ Key : aws .String (key ),
168+ })
169+ _ , err := deleteReq .Send (ctx )
128170 if err != nil {
129- log .With (zap .Error (err )).Warn ("failed to save currency metadata" )
130- return nil , status .Error (codes .Internal , "" )
171+ return errors .Wrap (err , "failed to delete icon from s3" )
131172 }
173+ return nil
174+ }
132175
133- return & currencypb.UpdateIconResponse {Result : currencypb .UpdateIconResponse_OK }, nil
176+ func iconKey (mint * common.Account , ext string ) string {
177+ return fmt .Sprintf ("%s/icon.%s" , mint .PublicKey ().ToBase58 (), ext )
134178}
0 commit comments