Skip to content

Commit abcfb31

Browse files
authored
Handle additional currency customization in Launch RPC (#153)
1 parent dce8845 commit abcfb31

2 files changed

Lines changed: 118 additions & 27 deletions

File tree

ocp/rpc/currency/icon.go

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2829
var (
2930
jpegMagic = []byte{0xFF, 0xD8, 0xFF}
3031
pngMagic = []byte{0x89, 0x50, 0x4E, 0x47}
32+
33+
errInvalidIcon = errors.New("invalid icon")
3134
)
3235

3336
func (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
}

ocp/rpc/currency/launch.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"google.golang.org/grpc/codes"
1111
"google.golang.org/grpc/status"
1212

13-
commonpb "github.com/code-payments/ocp-protobuf-api/generated/go/common/v1"
1413
currencypb "github.com/code-payments/ocp-protobuf-api/generated/go/currency/v1"
1514
"github.com/mr-tron/base58"
1615

@@ -77,6 +76,20 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque
7776
}
7877
}
7978

79+
description := strings.TrimSpace(req.Description)
80+
81+
var processedIcon []byte
82+
var iconExt, iconContentType string
83+
if len(req.Icon) > 0 {
84+
processedIcon, iconExt, iconContentType, err = processIcon(req.Icon)
85+
if err == errInvalidIcon {
86+
return &currencypb.LaunchResponse{Result: currencypb.LaunchResponse_INVALID_ICON}, nil
87+
} else if err != nil {
88+
log.With(zap.Error(err)).Warn("falied to process icon")
89+
return nil, status.Error(codes.Internal, "")
90+
}
91+
}
92+
8093
allow, err := s.antispamGuard.AllowCurrencyLaunch(ctx, ownerAccount, name, symbol)
8194
if err != nil {
8295
log.With(zap.Error(err)).Warn("failed to perform antispam checks")
@@ -106,6 +119,11 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque
106119
log.With(zap.Error(err)).Warn("failed to derive mint address")
107120
return nil, status.Error(codes.Internal, "")
108121
}
122+
targetMintAccount, err := common.NewAccountFromPublicKeyBytes(targetMintAddress)
123+
if err != nil {
124+
log.With(zap.Error(err)).Warn("invalid target mint addres")
125+
return nil, status.Error(codes.Internal, "")
126+
}
109127

110128
currencyAddress, currencyBump, err := currencycreator.GetCurrencyAddress(&currencycreator.GetCurrencyAddressArgs{
111129
Mint: targetMintAddress,
@@ -204,6 +222,17 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque
204222
CreatedAt: creationTs,
205223
}
206224

225+
if len(description) > 0 {
226+
currencyMetadataRecord.Description = description
227+
}
228+
if req.BillCustomization != nil {
229+
var billColors []string
230+
for _, billColor := range req.BillCustomization.Colors {
231+
billColors = append(billColors, billColor.Hex)
232+
}
233+
currencyMetadataRecord.BillColors = billColors
234+
}
235+
207236
vmMetadataRecord := &vm_metadata.Record{
208237
Mint: base58.Encode(targetMintAddress),
209238
Authority: authority.PublicKey().ToBase58(),
@@ -217,6 +246,17 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque
217246
}
218247

219248
if !isDryRun {
249+
var iconUploaded bool
250+
if len(processedIcon) > 0 {
251+
imageUrl, err := uploadIcon(ctx, s.s3Client, targetMintAccount, processedIcon, iconExt, iconContentType)
252+
if err != nil {
253+
log.With(zap.Error(err)).Warn("failed to upload icon to s3")
254+
return nil, status.Error(codes.Internal, "")
255+
}
256+
currencyMetadataRecord.ImageUrl = imageUrl
257+
iconUploaded = true
258+
}
259+
220260
err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
221261
err := s.data.SaveKey(ctx, &authorityVaultRecord)
222262
if err != nil {
@@ -230,16 +270,23 @@ func (s *currencyServer) Launch(ctx context.Context, req *currencypb.LaunchReque
230270

231271
return s.data.SaveVmMetadata(ctx, vmMetadataRecord)
232272
})
233-
if err == currency.ErrDuplicateCurrency {
234-
return &currencypb.LaunchResponse{Result: currencypb.LaunchResponse_NAME_EXISTS}, nil
235-
} else if err != nil {
273+
if err != nil {
274+
if iconUploaded {
275+
if err := deleteIcon(context.Background(), s.s3Client, targetMintAccount, iconExt); err != nil {
276+
log.With(zap.Error(err)).Warn("best-effort icon cleanup failed")
277+
}
278+
}
279+
280+
if err == currency.ErrDuplicateCurrency {
281+
return &currencypb.LaunchResponse{Result: currencypb.LaunchResponse_NAME_EXISTS}, nil
282+
}
236283
log.With(zap.Error(err)).Warn("failed to save currency and vm metadata")
237284
return nil, status.Error(codes.Internal, "")
238285
}
239286
}
240287

241288
return &currencypb.LaunchResponse{
242289
Result: currencypb.LaunchResponse_OK,
243-
Mint: &commonpb.SolanaAccountId{Value: targetMintAddress},
290+
Mint: targetMintAccount.ToProto(),
244291
}, nil
245292
}

0 commit comments

Comments
 (0)