Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/ybbus/jsonrpc v2.1.2+incompatible
go.uber.org/zap v1.27.1
golang.org/x/image v0.36.0
google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
)
Expand Down Expand Up @@ -82,7 +83,7 @@ require (
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/ini.v1 v1.51.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down Expand Up @@ -669,8 +671,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
8 changes: 5 additions & 3 deletions ocp/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ const (
CoreMintVmAccountPublicKey = "JACkaKsm2Rd6TNJwH4UB7G6tHrWUATJPTgNNnRVsg4ip"
CoreMintVmOmnibusPublicKey = "D8oUTXRvarxhx9cjYdFJqWAVj2rmzry58bS6JSTiQsv5"

DefaultCurrencyIconImageUrl = "https://flipcash-currency-assets.s3.us-east-1.amazonaws.com/default/icon.jpg"
CurrencyAssetsS3BucketName = "flipcash-currency-assets"
CurrencyAssetsS3BucketRegion = "us-east-1"
)

var (
CoreMintImageUrl = fmt.Sprintf("https://flipcash-currency-assets.s3.us-east-1.amazonaws.com/%s/icon.png", CoreMintPublicKeyString)
CoreMintImageUrl = fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s/icon.png", CurrencyAssetsS3BucketName, CurrencyAssetsS3BucketRegion, CoreMintPublicKeyString)
CoreMintPublicKeyBytes []byte

DefaultBillColors = []string{"#AAAAAA", "#2C2C2C"}
DefaultCurrencyIconImageUrl = fmt.Sprintf("https://%s.s3.%s.amazonaws.com/default/icon.jpg", CurrencyAssetsS3BucketName, CurrencyAssetsS3BucketRegion)
DefaultBillColors = []string{"#AAAAAA", "#2C2C2C"}
)

func init() {
Expand Down
135 changes: 135 additions & 0 deletions ocp/rpc/currency/icon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package currency

import (
"bytes"
"context"
"fmt"
"image"
"image/jpeg"
"image/png"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"go.uber.org/zap"
"golang.org/x/image/draw"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

currencypb "github.com/code-payments/ocp-protobuf-api/generated/go/currency/v1"

"github.com/code-payments/ocp-server/grpc/client"
"github.com/code-payments/ocp-server/ocp/common"
"github.com/code-payments/ocp-server/ocp/config"
"github.com/code-payments/ocp-server/ocp/data/currency"
)

const iconSize = 64

var (
jpegMagic = []byte{0xFF, 0xD8, 0xFF}
pngMagic = []byte{0x89, 0x50, 0x4E, 0x47}
)

func (s *currencyServer) UpdateIcon(ctx context.Context, req *currencypb.UpdateIconRequest) (*currencypb.UpdateIconResponse, error) {
log := s.log.With(zap.String("method", "UpdateIcon"))
log = client.InjectLoggingMetadata(ctx, log)

ownerAccount, err := common.NewAccountFromProto(req.Owner)
if err != nil {
log.With(zap.Error(err)).Warn("invalid owner address")
return nil, status.Error(codes.Internal, "")
}

signature := req.Signature
req.Signature = nil
err = s.auth.Authenticate(ctx, ownerAccount, req, signature)
if err != nil {
return nil, err
}

mintAccount, err := common.NewAccountFromProto(req.Mint)
if err != nil {
log.With(zap.Error(err)).Warn("invalid mint address")
return nil, status.Error(codes.Internal, "")
}
log = log.With(zap.String("mint", mintAccount.PublicKey().ToBase58()))

metadataRecord, err := s.data.GetCurrencyMetadata(ctx, mintAccount.PublicKey().ToBase58())
if err == currency.ErrNotFound {
return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_NOT_FOUND}, nil
} else if err != nil {
log.With(zap.Error(err)).Warn("failed to load currency metadata record")
return nil, status.Error(codes.Internal, "")
}
if metadataRecord.State != currency.MetadataStateAvailable {
return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_NOT_FOUND}, nil
}

if ownerAccount.PublicKey().ToBase58() != metadataRecord.CreatedBy {
return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_DENIED}, nil
}

var contentType string
var ext string
iconData := req.Icon
switch {
case len(iconData) >= len(jpegMagic) && bytes.Equal(iconData[:len(jpegMagic)], jpegMagic):
contentType = "image/jpeg"
ext = "jpg"
case len(iconData) >= len(pngMagic) && bytes.Equal(iconData[:len(pngMagic)], pngMagic):
contentType = "image/png"
ext = "png"
default:
return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_INVALID_ICON}, nil
}

src, _, err := image.Decode(bytes.NewReader(iconData))
if err != nil {
return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_INVALID_ICON}, nil
}

if bounds := src.Bounds(); bounds.Dx() != iconSize || bounds.Dy() != iconSize {
dst := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
draw.CatmullRom.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil)
src = dst
}

var encoded bytes.Buffer
switch ext {
case "jpg":
err = jpeg.Encode(&encoded, src, &jpeg.Options{Quality: 100})
case "png":
err = png.Encode(&encoded, src)
}
if err != nil {
log.With(zap.Error(err)).Warn("failed to encode resized icon")
return nil, status.Error(codes.Internal, "")
}

mint := mintAccount.PublicKey().ToBase58()
key := fmt.Sprintf("%s/icon.%s", mint, ext)
bucket := config.CurrencyAssetsS3BucketName

putReq := s.s3Client.PutObjectRequest(&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(encoded.Bytes()),
ContentType: aws.String(contentType),
ACL: s3.ObjectCannedACLPublicRead,
})
_, err = putReq.Send(ctx)
if err != nil {
log.With(zap.Error(err)).Warn("failed to upload icon to s3")
return nil, status.Error(codes.Internal, "")
}

metadataRecord.ImageUrl = fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", bucket, config.CurrencyAssetsS3BucketRegion, key)

err = s.data.SaveCurrencyMetadata(ctx, metadataRecord)
if err != nil {
log.With(zap.Error(err)).Warn("failed to save currency metadata")
return nil, status.Error(codes.Internal, "")
}

return &currencypb.UpdateIconResponse{Result: currencypb.UpdateIconResponse_OK}, nil
}
1 change: 1 addition & 0 deletions ocp/rpc/currency/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ func (s *currencyServer) UpdateMetadata(ctx context.Context, req *currencypb.Upd
log.With(zap.Error(err)).Warn("invalid mint address")
return nil, status.Error(codes.Internal, "")
}
log = log.With(zap.String("mint", mintAccount.PublicKey().ToBase58()))

metadataRecord, err := s.data.GetCurrencyMetadata(ctx, mintAccount.PublicKey().ToBase58())
if err == currency.ErrNotFound {
Expand Down
7 changes: 7 additions & 0 deletions ocp/rpc/currency/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (

"go.uber.org/zap"

"github.com/aws/aws-sdk-go-v2/service/s3"

currencypb "github.com/code-payments/ocp-protobuf-api/generated/go/currency/v1"

"github.com/code-payments/ocp-server/cache"
Expand All @@ -31,6 +33,8 @@ type currencyServer struct {

antispamGuard *antispam.Guard

s3Client *s3.Client

exchangeRateHistoryCache cache.Cache
reserveHistoryCache cache.Cache

Expand All @@ -46,6 +50,7 @@ func NewCurrencyServer(
log *zap.Logger,
data ocp_data.Provider,
antispamGuard *antispam.Guard,
s3Client *s3.Client,
configProvider ConfigProvider,
) currencypb.CurrencyServer {
conf := configProvider()
Expand All @@ -64,6 +69,8 @@ func NewCurrencyServer(

antispamGuard: antispamGuard,

s3Client: s3Client,

exchangeRateHistoryCache: cache.NewCache(1_000),
reserveHistoryCache: cache.NewCache(1_000),

Expand Down
Loading