diff --git a/cmd/devices-api/api.go b/cmd/devices-api/api.go index ca456abf..9ff162dd 100644 --- a/cmd/devices-api/api.go +++ b/cmd/devices-api/api.go @@ -171,16 +171,6 @@ func startWebAPI(logger zerolog.Logger, settings *config.Settings, pdb db.Store, v1.Get("/swagger/*", swagger.HandlerDefault) // Device Definitions nftController := controllers.NewNFTController(settings, pdb.DBS, &logger, s3NFTServiceClient, ddSvc, scTaskSvc, teslaTaskService, ddIntSvc, ddaSvc, ipfsSvc) - v1.Get("/vehicle/:tokenID", nftController.GetNFTMetadata) - v1.Get("/vehicle/:tokenID/image", nftController.GetNFTImage) - - v1.Get("/aftermarket/device/by-address/:address", nftController.GetAftermarketDeviceNFTMetadataByAddress) - v1.Get("/aftermarket/device/:tokenID", cacheHandler, nftController.GetAftermarketDeviceNFTMetadata) - v1.Get("/aftermarket/device/:tokenID/image", nftController.GetAftermarketDeviceNFTImage) - - v1.Get("/synthetic/device/:tokenID", nftController.GetSyntheticDeviceNFTMetadata) - - v1.Get("/integration/:tokenID", nftController.GetIntegrationNFTMetadata) v1.Get("/countries", countriesController.GetSupportedCountries) v1.Get("/countries/:countryCode", countriesController.GetCountry) diff --git a/internal/controllers/nft_controller.go b/internal/controllers/nft_controller.go index bf369615..d0c2bf84 100644 --- a/internal/controllers/nft_controller.go +++ b/internal/controllers/nft_controller.go @@ -4,15 +4,11 @@ import ( "context" "database/sql" "fmt" - "io" "math/big" "slices" - "strconv" - "strings" "github.com/DIMO-Network/devices-api/internal/services/ipfs" "github.com/DIMO-Network/devices-api/internal/services/registry" - "github.com/DIMO-Network/devices-api/internal/utils" "github.com/DIMO-Network/shared" "github.com/segmentio/ksuid" @@ -21,12 +17,9 @@ import ( "github.com/DIMO-Network/devices-api/internal/controllers/helpers" "github.com/DIMO-Network/devices-api/internal/services" "github.com/DIMO-Network/devices-api/models" - "github.com/DIMO-Network/go-mnemonic" pb "github.com/DIMO-Network/shared/api/users" "github.com/DIMO-Network/shared/db" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" - s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/ericlagergren/decimal" "github.com/ethereum/go-ethereum/common" "github.com/gofiber/fiber/v2" @@ -74,369 +67,6 @@ func NewNFTController(settings *config.Settings, dbs func() *db.ReaderWriter, lo } } -// GetNFTMetadata godoc -// @Description retrieves NFT metadata for a given tokenID -// @Tags nfts -// @Param tokenId path int true "token id" -// @Produce json -// @Success 200 {object} controllers.NFTMetadataResp -// @Failure 404 -// @Router /vehicle/{tokenId} [get] -func (nc *NFTController) GetNFTMetadata(c *fiber.Ctx) error { - tis := c.Params("tokenID") - ti, ok := new(big.Int).SetString(tis, 10) - if !ok { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Couldn't parse token id %q.", tis)) - } - tid := types.NewNullDecimal(new(decimal.Big).SetBigMantScale(ti, 0)) - - ud, err := models.UserDevices( - models.UserDeviceWhere.TokenID.EQ(tid), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - // Indexers start looking immediately. - helpers.SkipErrorLog(c) - return fiber.NewError(fiber.StatusNotFound, "NFT not found.") - } - nc.log.Err(err).Msg("Database error retrieving NFT metadata.") - return opaqueInternalError - } - - deviceDefinitionID := ud.DeviceDefinitionID - def, err := nc.deviceDefSvc.GetDeviceDefinitionByID(c.Context(), deviceDefinitionID) - if err != nil { - return shared.GrpcErrorToFiber(err, "failed to get device definition") - } - - description := fmt.Sprintf("%s %s %d", def.Make.Name, def.Type.Model, def.Type.Year) - - var name string - if ud.Name.Valid { - name = ud.Name.String - } else { - name = description - } - - imageURI := fmt.Sprintf("%s/v1/vehicle/%s/image", nc.Settings.DeploymentBaseURL, ti) - if ud.IpfsImageCid.Valid { - imageURI = ipfs.URL(ud.IpfsImageCid.String) - } - - return c.JSON(NFTMetadataResp{ - Name: name, - Description: description + ", a DIMO vehicle.", - Image: imageURI, - Attributes: []NFTAttribute{ - {TraitType: "Make", Value: def.Make.Name}, - {TraitType: "Model", Value: def.Type.Model}, - {TraitType: "Year", Value: strconv.Itoa(int(def.Type.Year))}, - }, - }) -} - -// GetIntegrationNFTMetadata godoc -// @Description gets an integration using its tokenID -// @Tags integrations -// @Produce json -// @Success 200 {array} controllers.NFTMetadataResp -// @Router /integration/{tokenID} [get] -func (nc *NFTController) GetIntegrationNFTMetadata(c *fiber.Ctx) error { - tokenID := c.Params("tokenID") - - uTokenID, err := strconv.ParseUint(tokenID, 10, 64) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "invalid tokenID provided") - } - - integration, err := nc.deviceDefSvc.GetIntegrationByTokenID(c.Context(), uTokenID) - if err != nil { - return shared.GrpcErrorToFiber(err, "failed to get integration") - } - - return c.JSON(NFTMetadataResp{ - Name: integration.Vendor, - Description: fmt.Sprintf("%s, a DIMO integration", integration.Vendor), - Attributes: []NFTAttribute{}, - }) -} - -type NFTMetadataResp struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Image string `json:"image,omitempty"` - Attributes []NFTAttribute `json:"attributes"` -} - -type NFTAttribute struct { - TraitType string `json:"trait_type"` - Value string `json:"value"` -} - -// GetNFTImage godoc -// @Description Returns the image for the given vehicle NFT. -// @Tags nfts -// @Param tokenId path int true "token id" -// @Param transparent query bool false "whether to remove the image background" -// @Produce png -// @Success 200 -// @Failure 404 -// @Router /vehicle/{tokenId}/image [get] -func (nc *NFTController) GetNFTImage(c *fiber.Ctx) error { - tis := c.Params("tokenID") - ti, ok := new(big.Int).SetString(tis, 10) - if !ok { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Couldn't parse token id %q.", tis)) - } - - transparent := c.Query("transparent") == "true" - tid := types.NewNullDecimal(new(decimal.Big).SetBigMantScale(ti, 0)) - - // todo: NFT not found errors here were getting hit a lot in prod - should we have a prometheus metric or we don't care? - nft, err := models.UserDevices( - models.UserDeviceWhere.TokenID.EQ(tid), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - helpers.SkipErrorLog(c) - return fiber.NewError(fiber.StatusNotFound, "NFT not found.") - } - nc.log.Err(err).Msg("Database error retrieving NFT metadata.") - return opaqueInternalError - } - - if nft.IpfsImageCid.Valid && !transparent { - imgB, err := nc.ipfsSvc.FetchImage(c.Context(), nft.IpfsImageCid.String) - if err != nil { - return fmt.Errorf("error fetching %q from IPFS gateway", nft.IpfsImageCid.String) - } - - c.Set("Content-Type", "image/png") - return c.Send(imgB) - } - - if !nft.MintRequestID.Valid { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No image available for vehicle %d.", ti)) - } - - imageName := nft.MintRequestID.String - suffix := ".png" - - if transparent { - suffix = "_transparent.png" - } - - s3o, err := nc.s3.GetObject(c.Context(), &s3.GetObjectInput{ - Bucket: aws.String(nc.Settings.NFTS3Bucket), - Key: aws.String(imageName + suffix), - }) - if err != nil { - if transparent { - var nsk *s3types.NoSuchKey - if errors.As(err, &nsk) { - // todo: this error was getting hit a lot in production - helpers.SkipErrorLog(c) - return fiber.NewError(fiber.StatusNotFound, "Transparent version not set.") - } - } - return fmt.Errorf("error retrieving image for vehicle %d from S3: %w", tid, err) - } - defer s3o.Body.Close() - - b, err := io.ReadAll(s3o.Body) - if err != nil { - return err - } - - c.Set("Content-Type", "image/png") - return c.Send(b) -} - -// GetAftermarketDeviceNFTMetadataByAddress godoc -// @Description Retrieves NFT metadata for a given aftermarket device, using the device's -// @Description Ethereum address. -// @Tags nfts -// @Param address path string true "Ethereum address for the device." -// @Produce json -// @Success 200 {object} controllers.NFTMetadataResp -// @Failure 404 -// @Router /aftermarket/device/by-address/{address} [get] -func (nc *NFTController) GetAftermarketDeviceNFTMetadataByAddress(c *fiber.Ctx) error { - maybeAddr := c.Params("address") - - if !common.IsHexAddress(maybeAddr) { - return fiber.NewError(fiber.StatusBadRequest, "Couldn't parse device address.") - } - - addr := common.HexToAddress(maybeAddr) - - ad, err := models.AftermarketDevices( - models.AftermarketDeviceWhere.EthereumAddress.EQ(addr.Bytes()), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fiber.NewError(fiber.StatusNotFound, "No device with that address.") - } - return err - } - - var name string - if three, err := mnemonic.EntropyToMnemonicThreeWords(ad.EthereumAddress); err == nil { - name = strings.Join(three, " ") - } - - return c.JSON(NFTMetadataResp{ - Name: name, - Description: name + ", a DIMO hardware device.", - Image: fmt.Sprintf("%s/v1/aftermarket/device/%s/image", nc.Settings.DeploymentBaseURL, ad.TokenID), - Attributes: []NFTAttribute{ - {TraitType: "Ethereum Address", Value: common.BytesToAddress(ad.EthereumAddress).String()}, - {TraitType: "Serial Number", Value: ad.Serial}, - }, - }) -} - -// GetAftermarketDeviceNFTMetadata godoc -// @Description Retrieves NFT metadata for a given aftermarket device. -// @Tags nfts -// @Param tokenId path int true "token id" -// @Produce json -// @Success 200 {object} controllers.NFTMetadataResp -// @Failure 404 -// @Router /aftermarket/device/{tokenId} [get] -func (nc *NFTController) GetAftermarketDeviceNFTMetadata(c *fiber.Ctx) error { - tidStr := c.Params("tokenID") - - tid, ok := new(big.Int).SetString(tidStr, 10) - if !ok { - return fiber.NewError(fiber.StatusBadRequest, "Couldn't parse token id.") - } - - unit, err := models.AftermarketDevices( - models.AftermarketDeviceWhere.TokenID.EQ(utils.BigToDecimal(tid)), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fiber.NewError(fiber.StatusNotFound, "No device with that id.") - } - return err - } - var name string - if three, err := mnemonic.EntropyToMnemonicThreeWords(unit.EthereumAddress); err == nil { - name = strings.Join(three, " ") - } - - return c.JSON(NFTMetadataResp{ - Name: name, - Description: name + ", a DIMO hardware device.", - Image: fmt.Sprintf("%s/v1/aftermarket/device/%s/image", nc.Settings.DeploymentBaseURL, tid), - Attributes: []NFTAttribute{ - {TraitType: "Ethereum Address", Value: common.BytesToAddress(unit.EthereumAddress).String()}, - {TraitType: "Serial Number", Value: unit.Serial}, - }, - }) -} - -// GetAftermarketDeviceNFTImage godoc -// @Description Returns the image for the given aftermarket device NFT. -// @Tags nfts -// @Param tokenId path int true "token id" -// @Produce png -// @Success 200 -// @Failure 404 -// @Router /aftermarket/device/{tokenId}/image [get] -func (nc *NFTController) GetAftermarketDeviceNFTImage(c *fiber.Ctx) error { - tis := c.Params("tokenID") - ti, ok := new(big.Int).SetString(tis, 10) - if !ok { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Couldn't parse token id %q.", tis)) - } - - ad, err := models.AftermarketDevices( - models.AftermarketDeviceWhere.TokenID.EQ(utils.BigToDecimal(ti)), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if err == sql.ErrNoRows { - return fiber.NewError(fiber.StatusNotFound, "No device with id.") - } - return err - } - - dm, err := nc.deviceDefSvc.GetMakeByTokenID(c.Context(), ad.DeviceManufacturerTokenID.Int(nil)) - if err != nil { - return err - } - - var key string - - switch dm.Name { - case constants.AutoPiVendor: - key = nc.Settings.AutoPiNFTImage - case "Hashdog": - key = nc.Settings.MacaronNFTImage - default: - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("No NFT image for manufacturer %s.", dm.Name)) - } - - s3o, err := nc.s3.GetObject(c.Context(), &s3.GetObjectInput{ - Bucket: aws.String(nc.Settings.NFTS3Bucket), - Key: aws.String(key), - }) - if err != nil { - nc.log.Err(err).Msg("Failure communicating with S3.") - return opaqueInternalError - } - defer s3o.Body.Close() - - b, err := io.ReadAll(s3o.Body) - if err != nil { - return err - } - - c.Set("Content-Type", "image/png") - return c.Send(b) -} - -// GetSyntheticDeviceNFTMetadata godoc -// @Description Retrieves NFT metadata for a given synthetic device. -// @Tags nfts -// @Param tokenId path int true "token id" -// @Produce json -// @Success 200 {object} controllers.NFTMetadataResp -// @Failure 404 -// @Router /synthetic/device/{tokenId} [get] -func (nc *NFTController) GetSyntheticDeviceNFTMetadata(c *fiber.Ctx) error { - tidStr := c.Params("tokenID") - - tid, ok := new(big.Int).SetString(tidStr, 10) - if !ok { - return fiber.NewError(fiber.StatusBadRequest, "Couldn't parse token id.") - } - - unit, err := models.SyntheticDevices( - models.SyntheticDeviceWhere.TokenID.EQ(utils.NullableBigToDecimal(tid)), - ).One(c.Context(), nc.DBS().Reader) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - helpers.SkipErrorLog(c) - return fiber.NewError(fiber.StatusNotFound, "No device with that id.") - } - return err - } - var name string - if three, err := mnemonic.EntropyToMnemonicThreeWords(unit.WalletAddress); err == nil { - name = strings.Join(three, " ") - } - - return c.JSON(NFTMetadataResp{ - Name: name, - Description: name + ", a DIMO synthetic device.", - Attributes: []NFTAttribute{ - {TraitType: "Ethereum Address", Value: common.BytesToAddress(unit.WalletAddress).String()}, - }, - }) -} - // UnlockDoors godoc // @Summary Unlock the device's doors // @Description Unlock the device's doors.