From beee1eb7d03b68f87ed87375e230ad9424a0d10d Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Tue, 8 Aug 2023 13:38:50 -0300 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=9A=80=20Create=20a=20TOML=20Genera?= =?UTF-8?q?tor=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 9 +- backend/config/config.go | 17 +- backend/docs/docs.go | 303 +++++++++++++++++- backend/docs/swagger.json | 303 +++++++++++++++++- backend/docs/swagger.yaml | 200 +++++++++++- backend/go.mod | 2 +- backend/internal/app/app.go | 6 +- backend/internal/controller/http/v1/assets.go | 52 +++ backend/internal/entity/asset.go | 72 +++++ backend/internal/usecase/assets.go | 19 +- backend/internal/usecase/interfaces.go | 10 +- backend/internal/usecase/mocks/mocks.go | 54 ++++ .../internal/usecase/repo/asset_postgres.go | 4 +- backend/main.go | 5 +- backend/pkg/toml/toml.go | 31 ++ 15 files changed, 1072 insertions(+), 15 deletions(-) create mode 100644 backend/pkg/toml/toml.go diff --git a/backend/.env.example b/backend/.env.example index 2a8834ad..a52f4637 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -22,4 +22,11 @@ HTTP_PORT= JWT_SECRET_KEY=secret ## Production environment -GIN_MODE=developer #release \ No newline at end of file +GIN_MODE=developer #release + +## Horizon Config +HORIZON_PUBLIC_API_SERVER=https://horizon-testnet.stellar.org +HORIZON_TEST_API_SERVER=https://horizon-testnet.stellar.org +PUBLIC_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 +TEST_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 +STELLAR_TOML_VERSION=2.5.0 \ No newline at end of file diff --git a/backend/config/config.go b/backend/config/config.go index ee2c5a98..db54ed74 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -9,10 +9,11 @@ import ( type ( Config struct { - Kafka KafkaConfig - PG PGConfig - HTTP HTTP - JWT JWT + Kafka KafkaConfig + PG PGConfig + HTTP HTTP + JWT JWT + Horizon Horizon } KafkaConfig struct { @@ -48,6 +49,14 @@ type ( JWT struct { SecretKey string `env-required:"true" env:"JWT_SECRET_KEY"` } + + Horizon struct { + PublicAPIServer string `env:"HORIZON_PUBLIC_API_SERVER"` + TestAPIServer string `env:"HORIZON_TEST_API_SERVER"` + PublicNetworkPass string `env:"PUBLIC_NETWORK_PASSPHRASE"` + TestNetworkPass string `env:"TEST_NETWORK_PASSPHRASE"` + StellarTomlVersion string `env:"STELLAR_TOML_VERSION"` + } ) // NewConfig returns app config. diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 00bc1692..ed903d7b 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -286,6 +286,52 @@ const docTemplate = `{ } } }, + "/assets/generate-toml": { + "post": { + "description": "Create a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Create a TOML file", + "parameters": [ + { + "description": "TOML info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.TomlData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/mint": { "post": { "description": "Mint an asset on Stellar", @@ -338,6 +384,44 @@ const docTemplate = `{ } } }, + "/assets/toml/{asset_issuer}": { + "get": { + "description": "Retrieve a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Retrieve a TOML file", + "parameters": [ + { + "type": "string", + "description": "Asset issuer", + "name": "asset_issuer", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/transfer": { "post": { "description": "Transfer an asset between wallets on Stellar", @@ -831,6 +915,127 @@ const docTemplate = `{ } } }, + "entity.Currency": { + "type": "object", + "properties": { + "anchorAsset": { + "type": "string" + }, + "anchorAssetType": { + "type": "string" + }, + "approvalCriteria": { + "type": "string" + }, + "approvalServer": { + "type": "string" + }, + "attestationOfReserve": { + "type": "string" + }, + "code": { + "type": "string" + }, + "collateralAddressMessages": { + "type": "array", + "items": { + "type": "string" + } + }, + "collateralAddressSignatures": { + "type": "array", + "items": { + "type": "string" + } + }, + "collateralAddresses": { + "type": "array", + "items": { + "type": "string" + } + }, + "conditions": { + "type": "string" + }, + "description": { + "type": "string" + }, + "displayDecimals": { + "type": "integer" + }, + "fixedNumber": { + "type": "integer" + }, + "image": { + "type": "string" + }, + "isAssetAnchored": { + "type": "boolean" + }, + "isUnlimited": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "maxNumber": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "redemptionInstructions": { + "type": "string" + }, + "regulated": { + "type": "boolean" + } + } + }, + "entity.Documentation": { + "type": "object", + "properties": { + "orgDBA": { + "type": "string" + }, + "orgDescription": { + "type": "string" + }, + "orgGithub": { + "type": "string" + }, + "orgKeybase": { + "type": "string" + }, + "orgLogo": { + "type": "string" + }, + "orgName": { + "type": "string" + }, + "orgOfficialEmail": { + "type": "string" + }, + "orgPhoneNumber": { + "type": "string" + }, + "orgPhoneNumberAttestation": { + "type": "string" + }, + "orgPhysicalAddress": { + "type": "string" + }, + "orgPhysicalAddressAttestation": { + "type": "string" + }, + "orgTwitter": { + "type": "string" + }, + "orgURL": { + "type": "string" + } + } + }, "entity.Key": { "type": "object", "properties": { @@ -852,6 +1057,32 @@ const docTemplate = `{ } } }, + "entity.Principal": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "github": { + "type": "string" + }, + "idphotoHash": { + "type": "string" + }, + "keybase": { + "type": "string" + }, + "name": { + "type": "string" + }, + "twitter": { + "type": "string" + }, + "verificationPhotoHash": { + "type": "string" + } + } + }, "entity.Role": { "type": "object", "properties": { @@ -868,7 +1099,7 @@ const docTemplate = `{ "entity.RolePermissionResponse": { "type": "object", "properties": { - "description": { + "action": { "type": "string", "example": "Edit action" }, @@ -878,6 +1109,56 @@ const docTemplate = `{ } } }, + "entity.TomlData": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "currencies": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Currency" + } + }, + "documentation": { + "$ref": "#/definitions/entity.Documentation" + }, + "federationServer": { + "type": "string" + }, + "horizonURL": { + "type": "string" + }, + "networkPassphrase": { + "type": "string" + }, + "principals": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Principal" + } + }, + "signingKey": { + "type": "string" + }, + "transferServer": { + "type": "string" + }, + "validators": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Validator" + } + }, + "version": { + "type": "string" + } + } + }, "entity.User": { "type": "object", "properties": { @@ -941,6 +1222,26 @@ const docTemplate = `{ } } }, + "entity.Validator": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "history": { + "type": "string" + }, + "host": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + } + }, "entity.Vault": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index b371cf0b..5fbd502d 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -274,6 +274,52 @@ } } }, + "/assets/generate-toml": { + "post": { + "description": "Create a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Create a TOML file", + "parameters": [ + { + "description": "TOML info", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entity.TomlData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/mint": { "post": { "description": "Mint an asset on Stellar", @@ -326,6 +372,44 @@ } } }, + "/assets/toml/{asset_issuer}": { + "get": { + "description": "Retrieve a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Retrieve a TOML file", + "parameters": [ + { + "type": "string", + "description": "Asset issuer", + "name": "asset_issuer", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/transfer": { "post": { "description": "Transfer an asset between wallets on Stellar", @@ -819,6 +903,127 @@ } } }, + "entity.Currency": { + "type": "object", + "properties": { + "anchorAsset": { + "type": "string" + }, + "anchorAssetType": { + "type": "string" + }, + "approvalCriteria": { + "type": "string" + }, + "approvalServer": { + "type": "string" + }, + "attestationOfReserve": { + "type": "string" + }, + "code": { + "type": "string" + }, + "collateralAddressMessages": { + "type": "array", + "items": { + "type": "string" + } + }, + "collateralAddressSignatures": { + "type": "array", + "items": { + "type": "string" + } + }, + "collateralAddresses": { + "type": "array", + "items": { + "type": "string" + } + }, + "conditions": { + "type": "string" + }, + "description": { + "type": "string" + }, + "displayDecimals": { + "type": "integer" + }, + "fixedNumber": { + "type": "integer" + }, + "image": { + "type": "string" + }, + "isAssetAnchored": { + "type": "boolean" + }, + "isUnlimited": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "maxNumber": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "redemptionInstructions": { + "type": "string" + }, + "regulated": { + "type": "boolean" + } + } + }, + "entity.Documentation": { + "type": "object", + "properties": { + "orgDBA": { + "type": "string" + }, + "orgDescription": { + "type": "string" + }, + "orgGithub": { + "type": "string" + }, + "orgKeybase": { + "type": "string" + }, + "orgLogo": { + "type": "string" + }, + "orgName": { + "type": "string" + }, + "orgOfficialEmail": { + "type": "string" + }, + "orgPhoneNumber": { + "type": "string" + }, + "orgPhoneNumberAttestation": { + "type": "string" + }, + "orgPhysicalAddress": { + "type": "string" + }, + "orgPhysicalAddressAttestation": { + "type": "string" + }, + "orgTwitter": { + "type": "string" + }, + "orgURL": { + "type": "string" + } + } + }, "entity.Key": { "type": "object", "properties": { @@ -840,6 +1045,32 @@ } } }, + "entity.Principal": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "github": { + "type": "string" + }, + "idphotoHash": { + "type": "string" + }, + "keybase": { + "type": "string" + }, + "name": { + "type": "string" + }, + "twitter": { + "type": "string" + }, + "verificationPhotoHash": { + "type": "string" + } + } + }, "entity.Role": { "type": "object", "properties": { @@ -856,7 +1087,7 @@ "entity.RolePermissionResponse": { "type": "object", "properties": { - "description": { + "action": { "type": "string", "example": "Edit action" }, @@ -866,6 +1097,56 @@ } } }, + "entity.TomlData": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "currencies": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Currency" + } + }, + "documentation": { + "$ref": "#/definitions/entity.Documentation" + }, + "federationServer": { + "type": "string" + }, + "horizonURL": { + "type": "string" + }, + "networkPassphrase": { + "type": "string" + }, + "principals": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Principal" + } + }, + "signingKey": { + "type": "string" + }, + "transferServer": { + "type": "string" + }, + "validators": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Validator" + } + }, + "version": { + "type": "string" + } + } + }, "entity.User": { "type": "object", "properties": { @@ -929,6 +1210,26 @@ } } }, + "entity.Validator": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "history": { + "type": "string" + }, + "host": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + } + }, "entity.Vault": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index b41f9fe5..1efef10a 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -20,6 +20,86 @@ definitions: example: USD Coin type: string type: object + entity.Currency: + properties: + anchorAsset: + type: string + anchorAssetType: + type: string + approvalCriteria: + type: string + approvalServer: + type: string + attestationOfReserve: + type: string + code: + type: string + collateralAddressMessages: + items: + type: string + type: array + collateralAddressSignatures: + items: + type: string + type: array + collateralAddresses: + items: + type: string + type: array + conditions: + type: string + description: + type: string + displayDecimals: + type: integer + fixedNumber: + type: integer + image: + type: string + isAssetAnchored: + type: boolean + isUnlimited: + type: boolean + issuer: + type: string + maxNumber: + type: integer + name: + type: string + redemptionInstructions: + type: string + regulated: + type: boolean + type: object + entity.Documentation: + properties: + orgDBA: + type: string + orgDescription: + type: string + orgGithub: + type: string + orgKeybase: + type: string + orgLogo: + type: string + orgName: + type: string + orgOfficialEmail: + type: string + orgPhoneNumber: + type: string + orgPhoneNumberAttestation: + type: string + orgPhysicalAddress: + type: string + orgPhysicalAddressAttestation: + type: string + orgTwitter: + type: string + orgURL: + type: string + type: object entity.Key: properties: id: @@ -35,6 +115,23 @@ definitions: example: 3 type: integer type: object + entity.Principal: + properties: + email: + type: string + github: + type: string + idphotoHash: + type: string + keybase: + type: string + name: + type: string + twitter: + type: string + verificationPhotoHash: + type: string + type: object entity.Role: properties: id: @@ -46,13 +143,46 @@ definitions: type: object entity.RolePermissionResponse: properties: - description: + action: example: Edit action type: string name: example: Edit type: string type: object + entity.TomlData: + properties: + accounts: + items: + type: string + type: array + currencies: + items: + $ref: '#/definitions/entity.Currency' + type: array + documentation: + $ref: '#/definitions/entity.Documentation' + federationServer: + type: string + horizonURL: + type: string + networkPassphrase: + type: string + principals: + items: + $ref: '#/definitions/entity.Principal' + type: array + signingKey: + type: string + transferServer: + type: string + validators: + items: + $ref: '#/definitions/entity.Validator' + type: array + version: + type: string + type: object entity.User: properties: created_at: @@ -94,6 +224,19 @@ definitions: id_user: type: string type: object + entity.Validator: + properties: + alias: + type: string + displayName: + type: string + history: + type: string + host: + type: string + publicKey: + type: string + type: object entity.Vault: properties: id: @@ -487,6 +630,36 @@ paths: summary: Clawback an asset tags: - Assets + /assets/generate-toml: + post: + consumes: + - application/json + description: Create a TOML file + parameters: + - description: TOML info + in: body + name: request + required: true + schema: + $ref: '#/definitions/entity.TomlData' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/v1.response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/v1.response' + summary: Create a TOML file + tags: + - Assets /assets/mint: post: consumes: @@ -521,6 +694,31 @@ paths: summary: Mint an asset tags: - Assets + /assets/toml/{asset_issuer}: + get: + consumes: + - application/json + description: Retrieve a TOML file + parameters: + - description: Asset issuer + in: path + name: asset_issuer + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/v1.response' + summary: Retrieve a TOML file + tags: + - Assets /assets/transfer: post: consumes: diff --git a/backend/go.mod b/backend/go.mod index b5ca8952..da2099de 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,6 +11,7 @@ require ( github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/pelletier/go-toml/v2 v2.0.9 github.com/stretchr/testify v1.8.4 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 @@ -56,7 +57,6 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 37a9079d..9ca2b6a8 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -17,6 +17,7 @@ import ( "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase/repo" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/httpserver" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/postgres" + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/toml" ) func CORSMiddleware() gin.HandlerFunc { @@ -36,9 +37,8 @@ func CORSMiddleware() gin.HandlerFunc { } // Run creates objects via constructors. -func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.ProducerInterface) { +func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.ProducerInterface, tRepo *toml.DefaultTomlGenerator) { // l := logger.New(cfg.Log.Level) - // Use cases authUc := usecase.NewAuthUseCase( repo.New(pg), cfg.JWT.SecretKey, @@ -52,6 +52,8 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.Produ assetUc := usecase.NewAssetUseCase( repo.NewAssetRepo(pg), repo.NewWalletRepo(pg), + tRepo, + cfg.Horizon, ) roleUc := usecase.NewRoleUseCase( repo.NewRoleRepo(pg), diff --git a/backend/internal/controller/http/v1/assets.go b/backend/internal/controller/http/v1/assets.go index a162c8a9..03adc8a2 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -32,6 +32,9 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/clawback", r.clawbackAsset) h.POST("/burn", r.burnAsset) h.POST("/transfer", r.transferAsset) + h.POST("/generate-toml", r.generateTOML) + h.GET("/retrieve-toml/:asset_issuer", r.retrieveToml) + } } @@ -582,3 +585,52 @@ func (r *assetsRoutes) getAllAssets(c *gin.Context) { c.JSON(http.StatusOK, assets) } + +// @Summary Create a TOML file +// @Description Create a TOML file +// @Tags Assets +// @Accept json +// @Produce json +// @Param request body entity.TomlData true "TOML info" +// @Success 200 {object} response[string] +// @Failure 400 {object} response +// @Failure 500 {object} response +// @Router /assets/generate-toml [post] +func (r *assetsRoutes) generateTOML(c *gin.Context) { + var request entity.TomlData + if err := c.ShouldBindJSON(&request); err != nil { + errorResponse(c, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error())) + return + } + + toml, err := r.as.CreateToml(request) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error creating TOML") + return + } + + c.Data(http.StatusOK, "application/toml", []byte(toml)) +} + +// @Summary Retrieve a TOML file +// @Description Retrieve a TOML file +// @Tags Assets +// @Accept json +// @Produce json +// @Param asset_issuer path string true "Asset issuer" +// @Success 200 {object} response[string] +// @Failure 500 {object} response +// @Router /assets/toml/{asset_issuer} [get] +func (r *assetsRoutes) retrieveToml(c *gin.Context) { + // Get the asset issuer from the request URL + assetIssuer := c.Param("asset_issuer") + + // Retrieve the TOML content for the specified asset issuer + tomlContent, err := r.as.RetrieveToml(assetIssuer) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error retrieving TOML") + return + } + + c.Data(http.StatusOK, "application/toml", []byte(tomlContent)) +} diff --git a/backend/internal/entity/asset.go b/backend/internal/entity/asset.go index 34a160d0..843c215f 100644 --- a/backend/internal/entity/asset.go +++ b/backend/internal/entity/asset.go @@ -17,3 +17,75 @@ const ( PaymentToken = "payment_token" DefiToken = "defi_token" ) + +type TomlData struct { + Version string `toml:"VERSION"` + NetworkPassphrase string `toml:"NETWORK_PASSPHRASE"` + FederationServer string `toml:"FEDERATION_SERVER"` + TransferServer string `toml:"TRANSFER_SERVER"` + SigningKey string `toml:"SIGNING_KEY"` + HorizonURL string `toml:"HORIZON_URL"` + Accounts []string `toml:"ACCOUNTS"` + Documentation Documentation `toml:"DOCUMENTATION"` + Principals []Principal `toml:"PRINCIPALS"` + Currencies []Currency `toml:"CURRENCIES"` + Validators []Validator `toml:"VALIDATORS"` +} + +type Documentation struct { + OrgName string `toml:"ORG_NAME"` + OrgDBA string `toml:"ORG_DBA"` + OrgURL string `toml:"ORG_URL"` + OrgLogo string `toml:"ORG_LOGO"` + OrgDescription string `toml:"ORG_DESCRIPTION"` + OrgPhysicalAddress string `toml:"ORG_PHYSICAL_ADDRESS"` + OrgPhysicalAddressAttestation string `toml:"ORG_PHYSICAL_ADDRESS_ATTESTATION"` + OrgPhoneNumber string `toml:"ORG_PHONE_NUMBER"` + OrgPhoneNumberAttestation string `toml:"ORG_PHONE_NUMBER_ATTESTATION"` + OrgKeybase string `toml:"ORG_KEYBASE"` + OrgTwitter string `toml:"ORG_TWITTER"` + OrgGithub string `toml:"ORG_GITHUB"` + OrgOfficialEmail string `toml:"ORG_OFFICIAL_EMAIL"` +} + +type Principal struct { + Name string `toml:"name"` + Email string `toml:"email"` + Keybase string `toml:"keybase"` + Twitter string `toml:"twitter"` + Github string `toml:"github"` + IDPhotoHash string `toml:"id_photo_hash"` + VerificationPhotoHash string `toml:"verification_photo_hash"` +} + +type Currency struct { + Code string `toml:"code"` + Issuer string `toml:"issuer"` + DisplayDecimals int `toml:"display_decimals"` + Name string `toml:"name"` + Description string `toml:"desc"` + Conditions string `toml:"conditions"` + Image string `toml:"image"` + FixedNumber int `toml:"fixed_number"` + MaxNumber int `toml:"max_number"` + IsUnlimited bool `toml:"is_unlimited"` + IsAssetAnchored bool `toml:"is_asset_anchored"` + AnchorAssetType string `toml:"anchor_asset_type"` + AnchorAsset string `toml:"anchor_asset"` + AttestationOfReserve string `toml:"attestation_of_reserve"` + RedemptionInstructions string `toml:"redemption_instructions"` + CollateralAddresses []string `toml:"collateral_addresses"` + CollateralAddressMessages []string `toml:"collateral_address_messages"` + CollateralAddressSignatures []string `toml:"collateral_address_signatures"` + Regulated bool `toml:"regulated"` + ApprovalServer string `toml:"approval_server"` + ApprovalCriteria string `toml:"approval_criteria"` +} + +type Validator struct { + Alias string `toml:"ALIAS"` + DisplayName string `toml:"DISPLAY_NAME"` + Host string `toml:"HOST"` + PublicKey string `toml:"PUBLIC_KEY"` + History string `toml:"HISTORY"` +} diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index 7ed7ef9f..dc303b6e 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -3,18 +3,23 @@ package usecase import ( "fmt" + "github.com/CheesecakeLabs/token-factory-v2/backend/config" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" ) type AssetUseCase struct { aRepo AssetRepoInterface wRepo WalletRepoInterface + tRepo TomlInterface + cfg config.Horizon } -func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface) *AssetUseCase { +func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface, tRepo TomlInterface, cfg config.Horizon) *AssetUseCase { return &AssetUseCase{ aRepo: aRepo, wRepo: wRepo, + tRepo: tRepo, + cfg: cfg, } } @@ -65,3 +70,15 @@ func (uc *AssetUseCase) GetAll() ([]entity.Asset, error) { return assets, nil } + +func (uc *AssetUseCase) CreateToml(req entity.TomlData) (string, error) { + toml, err := uc.tRepo.GenerateToml(req, uc.cfg) + + return toml, err +} + +func (uc *AssetUseCase) RetrieveToml(code string) (string, error) { + toml, err := uc.tRepo.RetrieveToml(code) + + return toml, err +} diff --git a/backend/internal/usecase/interfaces.go b/backend/internal/usecase/interfaces.go index a8f5075a..b1045225 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -1,6 +1,9 @@ package usecase -import "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/config" + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" +) // mockgen -source=internal/usecase/interfaces.go -destination=internal/usecase/mocks/mocks.go -package=mocks @@ -55,6 +58,11 @@ type ( GetRolePermissions(token string) ([]entity.RolePermissionResponse, error) } + TomlInterface interface { + GenerateToml(entity.TomlData, config.Horizon) (string, error) + RetrieveToml(string) (string, error) + } + VaultCategoryRepoInterface interface { GetVaultCategories() ([]entity.VaultCategory, error) GetVaultCategoryById(id int) (entity.VaultCategory, error) diff --git a/backend/internal/usecase/mocks/mocks.go b/backend/internal/usecase/mocks/mocks.go index 52f380b2..ef0bb20e 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -7,6 +7,7 @@ package mocks import ( reflect "reflect" + config "github.com/CheesecakeLabs/token-factory-v2/backend/config" entity "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" usecase "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase" gomock "github.com/golang/mock/gomock" @@ -535,6 +536,59 @@ func (mr *MockRolePermissionRepoInterfaceMockRecorder) Validate(action, roleId i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockRolePermissionRepoInterface)(nil).Validate), action, roleId) } +// MockTomlInterface is a mock of TomlInterface interface. +type MockTomlInterface struct { + ctrl *gomock.Controller + recorder *MockTomlInterfaceMockRecorder +} + +// MockTomlInterfaceMockRecorder is the mock recorder for MockTomlInterface. +type MockTomlInterfaceMockRecorder struct { + mock *MockTomlInterface +} + +// NewMockTomlInterface creates a new mock instance. +func NewMockTomlInterface(ctrl *gomock.Controller) *MockTomlInterface { + mock := &MockTomlInterface{ctrl: ctrl} + mock.recorder = &MockTomlInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTomlInterface) EXPECT() *MockTomlInterfaceMockRecorder { + return m.recorder +} + +// GenerateToml mocks base method. +func (m *MockTomlInterface) GenerateToml(arg0 entity.TomlData, arg1 config.Horizon) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateToml", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateToml indicates an expected call of GenerateToml. +func (mr *MockTomlInterfaceMockRecorder) GenerateToml(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateToml", reflect.TypeOf((*MockTomlInterface)(nil).GenerateToml), arg0, arg1) +} + +// RetrieveToml mocks base method. +func (m *MockTomlInterface) RetrieveToml(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RetrieveToml", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetrieveToml indicates an expected call of RetrieveToml. +func (mr *MockTomlInterfaceMockRecorder) RetrieveToml(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveToml", reflect.TypeOf((*MockTomlInterface)(nil).RetrieveToml), arg0) +} + // MockVaultCategoryRepoInterface is a mock of VaultCategoryRepoInterface interface. type MockVaultCategoryRepoInterface struct { ctrl *gomock.Controller diff --git a/backend/internal/usecase/repo/asset_postgres.go b/backend/internal/usecase/repo/asset_postgres.go index 88694eef..87a9464e 100644 --- a/backend/internal/usecase/repo/asset_postgres.go +++ b/backend/internal/usecase/repo/asset_postgres.go @@ -13,7 +13,9 @@ type AssetRepo struct { } func NewAssetRepo(pg *postgres.Postgres) AssetRepo { - return AssetRepo{pg} + return AssetRepo{ + Postgres: pg, + } } func (r AssetRepo) GetAsset(id int) (entity.Asset, error) { diff --git a/backend/main.go b/backend/main.go index 09c8a135..bfc37726 100644 --- a/backend/main.go +++ b/backend/main.go @@ -10,6 +10,7 @@ import ( "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/kafka" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/postgres" + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/toml" ) func main() { @@ -18,6 +19,8 @@ func main() { if err != nil { log.Fatalf("Config error: %s", err) } + // Toml + tRepo := toml.NewTomlGenerator() // Postgres pg, err := postgres.New(cfg.PG) @@ -53,5 +56,5 @@ func main() { } go envConn.Run(cfg, entity.EnvelopeChannel) - app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer) + app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer, tRepo) } diff --git a/backend/pkg/toml/toml.go b/backend/pkg/toml/toml.go new file mode 100644 index 00000000..5e31fe93 --- /dev/null +++ b/backend/pkg/toml/toml.go @@ -0,0 +1,31 @@ +package toml + +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/config" + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" + "github.com/pelletier/go-toml/v2" +) + +type DefaultTomlGenerator struct{} + +func NewTomlGenerator() *DefaultTomlGenerator { + return &DefaultTomlGenerator{} +} + +func (g *DefaultTomlGenerator) GenerateToml(req entity.TomlData, cfg config.Horizon) (string, error) { + if req.Version == "" { + req.Version = cfg.StellarTomlVersion + } + if req.NetworkPassphrase == "" { + req.NetworkPassphrase = cfg.TestNetworkPass + } + tomlBytes, err := toml.Marshal(req) + if err != nil { + return "", err + } + return string(tomlBytes), nil +} + +func (g *DefaultTomlGenerator) RetrieveToml(asset string) (string, error) { + return "", nil +} From fe514460dfd2e56d657f10f0b54ec5c9bde17953 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Tue, 8 Aug 2023 15:29:16 -0300 Subject: [PATCH 02/16] =?UTF-8?q?=E2=9C=85=20=20Add=20Generate=20TOML=20FI?= =?UTF-8?q?le=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/usecase/assets_test.go | 127 +++++++++++++++++- .../internal/usecase/repo/user_postgres.go | 1 - 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/backend/internal/usecase/assets_test.go b/backend/internal/usecase/assets_test.go index 8cebd790..c46a1dc5 100644 --- a/backend/internal/usecase/assets_test.go +++ b/backend/internal/usecase/assets_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/CheesecakeLabs/token-factory-v2/backend/config" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase/mocks" @@ -25,7 +26,7 @@ type testAsset struct { err error } -func asset(t *testing.T) (*usecase.AssetUseCase, *mocks.MockAssetRepoInterface, *mocks.MockWalletRepoInterface) { +func asset(t *testing.T) (*usecase.AssetUseCase, *mocks.MockAssetRepoInterface, *mocks.MockWalletRepoInterface, *mocks.MockTomlInterface) { t.Helper() mockCtl := gomock.NewController(t) @@ -33,13 +34,14 @@ func asset(t *testing.T) (*usecase.AssetUseCase, *mocks.MockAssetRepoInterface, rw := mocks.NewMockWalletRepoInterface(mockCtl) ra := mocks.NewMockAssetRepoInterface(mockCtl) - u := usecase.NewAssetUseCase(ra, rw) + tg := mocks.NewMockTomlInterface(mockCtl) + u := usecase.NewAssetUseCase(ra, rw, tg, config.Horizon{}) - return u, ra, rw + return u, ra, rw, tg } func TestAssetUseCaseCreate(t *testing.T) { - u, ra, rw := asset(t) + u, ra, rw, tg := asset(t) req := entity.Asset{ Code: "ABC", @@ -200,4 +202,121 @@ func TestAssetUseCaseCreate(t *testing.T) { } }) } + + assetID := 123 + mockAsset := entity.Asset{ + Id: assetID, + Code: "ABC", + } + + mockAssets := []entity.Asset{ + mockAsset, + } + + mockToml := "mock toml data" + mockReq := entity.TomlData{ + Version: "2.0.0", + } + + tests = []testAsset{ + // Test for GetById + { + name: "get by id - success", + req: assetID, + mock: func() { + ra.EXPECT().GetAssetById(assetID).Return(mockAsset, nil) + }, + res: mockAsset, + err: nil, + }, + // Add other test cases for GetById... + { + name: "get by id - database error", + req: assetID, + mock: func() { + ra.EXPECT().GetAssetById(assetID).Return(entity.Asset{}, assetDbError) + }, + res: entity.Asset{}, + err: assetDbError, + }, + // Test for GetAll + { + name: "get all - success", + mock: func() { + ra.EXPECT().GetAssets().Return(mockAssets, nil) + }, + res: mockAssets, + err: nil, + }, + { + name: "get all - database error", + mock: func() { + ra.EXPECT().GetAssets().Return([]entity.Asset{}, assetDbError) + }, + res: []entity.Asset{}, + err: assetDbError, + }, + { + name: "create toml - success", + req: mockReq, + mock: func() { + mockCfg := config.Horizon{} // Mock configuration + mockTRepo := tg + mockTRepo.EXPECT().GenerateToml(mockReq, mockCfg).Return(mockToml, nil) + }, + res: mockToml, + err: nil, + }, + { + name: "create toml - error", + req: mockReq, + mock: func() { + mockCfg := config.Horizon{} // Mock configuration + mockTRepo := tg + mockTRepo.EXPECT().GenerateToml(mockReq, mockCfg).Return("", errors.New("error")) + }, + res: "", + err: errors.New("error"), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + tc.mock() + + // Test GetById + if assetID, ok := tc.req.(string); ok { + res, err := u.GetById(assetID) + require.EqualValues(t, tc.res, res) + if tc.err == nil { + require.EqualValues(t, err, tc.err) + } else { + require.ErrorContains(t, err, tc.err.Error()) + } + } + + // Test GetAll + if _, ok := tc.req.(bool); ok { + res, err := u.GetAll() + require.EqualValues(t, tc.res, res) + if tc.err == nil { + require.EqualValues(t, err, tc.err) + } else { + require.ErrorContains(t, err, tc.err.Error()) + } + } + + // Test CreateToml + if req, ok := tc.req.(entity.TomlData); ok { + res, err := u.CreateToml(req) + require.EqualValues(t, tc.res, res) + if tc.err == nil { + require.EqualValues(t, err, tc.err) + } else { + require.ErrorContains(t, err, tc.err.Error()) + } + } + }) + } } diff --git a/backend/internal/usecase/repo/user_postgres.go b/backend/internal/usecase/repo/user_postgres.go index b3379786..45a6887a 100644 --- a/backend/internal/usecase/repo/user_postgres.go +++ b/backend/internal/usecase/repo/user_postgres.go @@ -99,7 +99,6 @@ func (r UserRepo) GetAllUsers() ([]entity.UserResponse, error) { ORDER BY u.name ASC` rows, err := r.Db.Query(stmt) - if err != nil { return nil, fmt.Errorf("UserRepo - GetAllUsers - db.Query: %w", err) } From 793b731754ab7c3ac9880b74dcf7c6af342ad4ad Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 13:24:25 -0300 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Add=20new=20migra?= =?UTF-8?q?tions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/migrations/000014_toml.down.sql | 3 +++ backend/migrations/000014_toml.up.sql | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 backend/migrations/000014_toml.down.sql create mode 100644 backend/migrations/000014_toml.up.sql diff --git a/backend/migrations/000014_toml.down.sql b/backend/migrations/000014_toml.down.sql new file mode 100644 index 00000000..37c630d2 --- /dev/null +++ b/backend/migrations/000014_toml.down.sql @@ -0,0 +1,3 @@ +DROP TRIGGER IF EXISTS trigger_update_updated_at ON toml; +DROP FUNCTION IF EXISTS update_updated_at(); +DROP TABLE IF EXISTS toml; \ No newline at end of file diff --git a/backend/migrations/000014_toml.up.sql b/backend/migrations/000014_toml.up.sql new file mode 100644 index 00000000..0dd1258e --- /dev/null +++ b/backend/migrations/000014_toml.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE toml ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Attach the trigger to the 'toml' table +CREATE TRIGGER trigger_update_updated_at +BEFORE UPDATE ON toml +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); \ No newline at end of file From fe89803cabd3c227be2f9fecd0e9a187ef42a4a1 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 13:24:52 -0300 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=93=9D=20=20Update=20Swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 1 + backend/config/config.go | 3 +- backend/docs/docs.go | 124 +++++++++++++------------------------- backend/docs/swagger.json | 124 +++++++++++++------------------------- backend/docs/swagger.yaml | 85 +++++++++----------------- backend/go.mod | 1 + backend/go.sum | 4 ++ 7 files changed, 117 insertions(+), 225 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index a52f4637..aec1b969 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,6 +18,7 @@ KAFKA_SCHEMA_REGISTRY_URL= # HTTP HTTP_PORT= +HTTP_FRONT_ADRESS= ## JWT SECRET JWT_SECRET_KEY=secret diff --git a/backend/config/config.go b/backend/config/config.go index db54ed74..aa03f0b7 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -43,7 +43,8 @@ type ( } HTTP struct { - Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` + Port string `env-required:"true" yaml:"port" env:"HTTP_PORT"` + FrontEndAdress string `env-required:"true" yaml:"front_adress" env:"HTTP_FRONT_ADRESS"` } JWT struct { diff --git a/backend/docs/docs.go b/backend/docs/docs.go index ed903d7b..ee41ea97 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -48,6 +48,44 @@ const docTemplate = `{ } } }, + "/.well-known/stellar.toml": { + "get": { + "description": "Retrieve a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Retrieve a TOML file", + "parameters": [ + { + "type": "string", + "description": "Asset issuer", + "name": "asset_issuer", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/entity.TomlData" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets": { "get": { "description": "Get all assets", @@ -314,7 +352,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.response" + "$ref": "#/definitions/entity.TomlData" } }, "400": { @@ -384,44 +422,6 @@ const docTemplate = `{ } } }, - "/assets/toml/{asset_issuer}": { - "get": { - "description": "Retrieve a TOML file", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Assets" - ], - "summary": "Retrieve a TOML file", - "parameters": [ - { - "type": "string", - "description": "Asset issuer", - "name": "asset_issuer", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/v1.response" - } - } - } - } - }, "/assets/transfer": { "post": { "description": "Transfer an asset between wallets on Stellar", @@ -663,7 +663,7 @@ const docTemplate = `{ "summary": "Create a new vault", "parameters": [ { - "description": "Vault info", + "description": "CreateVaultRequest", "name": "request", "in": "body", "required": true, @@ -1341,49 +1341,7 @@ const docTemplate = `{ } }, "v1.CreateAssetRequest": { - "type": "object", - "required": [ - "asset_type", - "code", - "name" - ], - "properties": { - "amount": { - "type": "string", - "example": "1000" - }, - "asset_type": { - "type": "string", - "example": "security_token" - }, - "code": { - "type": "string", - "example": "USDC" - }, - "limit": { - "type": "integer", - "example": 1000 - }, - "name": { - "type": "string", - "example": "USDC" - }, - "set_flags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "[\"AUTH_REQUIRED_FLAGS\"", - " \"AUTH_REVOCABLE_FLAGS\"", - "\"AUTH_CLAWBACK_ENABLED\"]" - ] - }, - "sponsor_id": { - "type": "integer", - "example": 2 - } - } + "type": "object" }, "v1.CreateVaultCategoryRequest": { "type": "object", diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 5fbd502d..3e2dff98 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -36,6 +36,44 @@ } } }, + "/.well-known/stellar.toml": { + "get": { + "description": "Retrieve a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Retrieve a TOML file", + "parameters": [ + { + "type": "string", + "description": "Asset issuer", + "name": "asset_issuer", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/entity.TomlData" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets": { "get": { "description": "Get all assets", @@ -302,7 +340,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.response" + "$ref": "#/definitions/entity.TomlData" } }, "400": { @@ -372,44 +410,6 @@ } } }, - "/assets/toml/{asset_issuer}": { - "get": { - "description": "Retrieve a TOML file", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Assets" - ], - "summary": "Retrieve a TOML file", - "parameters": [ - { - "type": "string", - "description": "Asset issuer", - "name": "asset_issuer", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/v1.response" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/v1.response" - } - } - } - } - }, "/assets/transfer": { "post": { "description": "Transfer an asset between wallets on Stellar", @@ -651,7 +651,7 @@ "summary": "Create a new vault", "parameters": [ { - "description": "Vault info", + "description": "CreateVaultRequest", "name": "request", "in": "body", "required": true, @@ -1329,49 +1329,7 @@ } }, "v1.CreateAssetRequest": { - "type": "object", - "required": [ - "asset_type", - "code", - "name" - ], - "properties": { - "amount": { - "type": "string", - "example": "1000" - }, - "asset_type": { - "type": "string", - "example": "security_token" - }, - "code": { - "type": "string", - "example": "USDC" - }, - "limit": { - "type": "integer", - "example": 1000 - }, - "name": { - "type": "string", - "example": "USDC" - }, - "set_flags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "[\"AUTH_REQUIRED_FLAGS\"", - " \"AUTH_REVOCABLE_FLAGS\"", - "\"AUTH_CLAWBACK_ENABLED\"]" - ] - }, - "sponsor_id": { - "type": "integer", - "example": 2 - } - } + "type": "object" }, "v1.CreateVaultCategoryRequest": { "type": "object", diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 1efef10a..ad19af6e 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -307,37 +307,6 @@ definitions: - from type: object v1.CreateAssetRequest: - properties: - amount: - example: "1000" - type: string - asset_type: - example: security_token - type: string - code: - example: USDC - type: string - limit: - example: 1000 - type: integer - name: - example: USDC - type: string - set_flags: - example: - - '["AUTH_REQUIRED_FLAGS"' - - ' "AUTH_REVOCABLE_FLAGS"' - - '"AUTH_CLAWBACK_ENABLED"]' - items: - type: string - type: array - sponsor_id: - example: 2 - type: integer - required: - - asset_type - - code - - name type: object v1.CreateVaultCategoryRequest: properties: @@ -474,6 +443,31 @@ paths: summary: Get all vault categories tags: - Vault category + /.well-known/stellar.toml: + get: + consumes: + - application/json + description: Retrieve a TOML file + parameters: + - description: Asset issuer + in: path + name: asset_issuer + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/entity.TomlData' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/v1.response' + summary: Retrieve a TOML file + tags: + - Assets /assets: get: consumes: @@ -648,7 +642,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/v1.response' + $ref: '#/definitions/entity.TomlData' "400": description: Bad Request schema: @@ -694,31 +688,6 @@ paths: summary: Mint an asset tags: - Assets - /assets/toml/{asset_issuer}: - get: - consumes: - - application/json - description: Retrieve a TOML file - parameters: - - description: Asset issuer - in: path - name: asset_issuer - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/v1.response' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/v1.response' - summary: Retrieve a TOML file - tags: - - Assets /assets/transfer: post: consumes: @@ -874,7 +843,7 @@ paths: - application/json description: Create and issue a new asset on Stellar parameters: - - description: Vault info + - description: CreateVaultRequest in: body name: request required: true diff --git a/backend/go.mod b/backend/go.mod index da2099de..0ed37181 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -77,6 +77,7 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/Eun/go-hit v0.5.23 github.com/bitly/go-notify v0.0.0-20130217044602-0a148b8111d6 + github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index e676e804..23d8611b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -19,6 +19,8 @@ github.com/Eun/yaegi-template v1.5.18/go.mod h1:iVHjge496SWL7hLf1euBZIO40Bk0R38g github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/aaw/maybe_tls v0.0.0-20160803104303-89c499bcc6aa h1:6yJyU8MlPBB2enGJdPciPlr8P+PC0nhCFHnSHYMirZI= github.com/aaw/maybe_tls v0.0.0-20160803104303-89c499bcc6aa/go.mod h1:I0wzMZvViQzmJjxK+AtfFAnqDCkQV/+r17PO1CCSYnU= github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= @@ -85,6 +87,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE= +github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= From 340df92cc352ea097e38617a9ca9b6739645ee9b Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 13:26:25 -0300 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=8D=B1=20Update=20the=20create=20as?= =?UTF-8?q?set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/app.go | 20 +-- backend/internal/controller/http/v1/assets.go | 51 +++--- backend/internal/controller/http/v1/router.go | 51 +++++- backend/internal/controller/http/v1/vault.go | 7 +- backend/internal/entity/asset.go | 7 + backend/internal/usecase/assets.go | 146 +++++++++++++++++- backend/internal/usecase/assets_test.go | 3 +- backend/internal/usecase/interfaces.go | 7 +- backend/internal/usecase/mocks/mocks.go | 57 ++++++- .../internal/usecase/repo/toml_postgres.go | 37 +++++ .../internal/usecase/repo/wallet_postgres.go | 20 +-- backend/internal/usecase/wallets.go | 8 + backend/pkg/toml/toml.go | 9 +- 13 files changed, 352 insertions(+), 71 deletions(-) create mode 100644 backend/internal/usecase/repo/toml_postgres.go diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 9ca2b6a8..db05c7f8 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -20,22 +20,6 @@ import ( "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/toml" ) -func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("Access-Control-Allow-Origin", "http://localhost:3000") - c.Header("Access-Control-Allow-Credentials", "true") - c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") - c.Header("Access-Control-Allow-Methods", "GET, POST, HEAD, PATCH, OPTIONS, PUT") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } -} - // Run creates objects via constructors. func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.ProducerInterface, tRepo *toml.DefaultTomlGenerator) { // l := logger.New(cfg.Log.Level) @@ -53,6 +37,7 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.Produ repo.NewAssetRepo(pg), repo.NewWalletRepo(pg), tRepo, + repo.NewTomlRepo(pg), cfg.Horizon, ) roleUc := usecase.NewRoleUseCase( @@ -72,8 +57,7 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.Produ // HTTP Server handler := gin.Default() - handler.Use(CORSMiddleware()) - v1.NewRouter(handler, pKp, pHor, pEnv, *authUc, *userUc, *walletUc, *assetUc, *roleUc, *rolePermissionUc, *vaultCategoryUc, *vaultUc) + v1.NewRouter(handler, pKp, pHor, pEnv, *authUc, *userUc, *walletUc, *assetUc, *roleUc, *rolePermissionUc, *vaultCategoryUc, *vaultUc, cfg.HTTP) httpServer := httpserver.New(handler, httpserver.Port(cfg.HTTP.Port), httpserver.ReadTimeout(60*time.Second), diff --git a/backend/internal/controller/http/v1/assets.go b/backend/internal/controller/http/v1/assets.go index 03adc8a2..040b6768 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -21,6 +21,14 @@ type assetsRoutes struct { a usecase.AuthUseCase } +func newAssetTomlRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as usecase.AssetUseCase, m HTTPControllerMessenger, a usecase.AuthUseCase) { + r := &assetsRoutes{w, as, m, a} + h := handler.Group("/").Use() + { + h.GET("/.well-known/stellar.toml", r.retrieveToml) + } +} + func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as usecase.AssetUseCase, m HTTPControllerMessenger, a usecase.AuthUseCase) { r := &assetsRoutes{w, as, m, a} h := handler.Group("/assets").Use(Auth(r.a.ValidateToken())) @@ -33,19 +41,19 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/burn", r.burnAsset) h.POST("/transfer", r.transferAsset) h.POST("/generate-toml", r.generateTOML) - h.GET("/retrieve-toml/:asset_issuer", r.retrieveToml) - } } type CreateAssetRequest struct { - SponsorId int `json:"sponsor_id" example:"2"` - Name string `json:"name" binding:"required" example:"USDC"` - AssetType string `json:"asset_type" binding:"required" example:"security_token"` - Code string `json:"code" binding:"required" example:"USDC"` - Limit *int `json:"limit" example:"1000"` - Amount string `json:"amount" example:"1000"` - SetFlags []string `json:"set_flags" example:"[\"AUTH_REQUIRED_FLAGS\", \"AUTH_REVOCABLE_FLAGS\",\"AUTH_CLAWBACK_ENABLED\"]"` + SponsorId int `json:"sponsor_id" example:"2"` + Name string `json:"name" binding:"required" example:"USDC"` + AssetType string `json:"asset_type" binding:"required" example:"security_token"` + Code string `json:"code" binding:"required" example:"USDC"` + Limit *int `json:"limit" example:"1000"` + Amount string `json:"amount" example:"1000"` + SetFlags []string `json:"set_flags" example:"[\"AUTH_REQUIRED_FLAGS\", \"AUTH_REVOCABLE_FLAGS\",\"AUTH_CLAWBACK_ENABLED\"]"` + SetToml bool `json:"set_toml" example:"true"` + TomlData entity.TomlData `json:"toml_data" example:"Example entity.TomlData"` } type BurnAssetRequest struct { @@ -224,6 +232,17 @@ func (r *assetsRoutes) createAsset(c *gin.Context) { return } + // See if we need to create a TOML file + if !request.SetToml { + c.JSON(http.StatusOK, asset) + } + + _, err = r.as.UpdateToml(request.TomlData) + if err != nil { + errorResponse(c, http.StatusNotFound, "error to retrieve TOML ") + return + } + c.JSON(http.StatusOK, asset) } @@ -592,7 +611,7 @@ func (r *assetsRoutes) getAllAssets(c *gin.Context) { // @Accept json // @Produce json // @Param request body entity.TomlData true "TOML info" -// @Success 200 {object} response[string] +// @Success 200 {object} entity.TomlData // @Failure 400 {object} response // @Failure 500 {object} response // @Router /assets/generate-toml [post] @@ -618,19 +637,15 @@ func (r *assetsRoutes) generateTOML(c *gin.Context) { // @Accept json // @Produce json // @Param asset_issuer path string true "Asset issuer" -// @Success 200 {object} response[string] +// @Success 200 {object} entity.TomlData // @Failure 500 {object} response -// @Router /assets/toml/{asset_issuer} [get] +// @Router /.well-known/stellar.toml [get] func (r *assetsRoutes) retrieveToml(c *gin.Context) { - // Get the asset issuer from the request URL - assetIssuer := c.Param("asset_issuer") - - // Retrieve the TOML content for the specified asset issuer - tomlContent, err := r.as.RetrieveToml(assetIssuer) + tomlContent, err := r.as.RetrieveToml() if err != nil { errorResponse(c, http.StatusInternalServerError, "error retrieving TOML") return } - c.Data(http.StatusOK, "application/toml", []byte(tomlContent)) + c.Data(http.StatusOK, "text/plain", []byte(tomlContent)) } diff --git a/backend/internal/controller/http/v1/router.go b/backend/internal/controller/http/v1/router.go index 831a2d1c..ba2baf71 100644 --- a/backend/internal/controller/http/v1/router.go +++ b/backend/internal/controller/http/v1/router.go @@ -3,19 +3,50 @@ package v1 import ( "net/http" + "github.com/CheesecakeLabs/token-factory-v2/backend/config" + docs "github.com/CheesecakeLabs/token-factory-v2/backend/docs" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" - - docs "github.com/CheesecakeLabs/token-factory-v2/backend/docs" ) +func CORSMiddlewareAllowAllOrigins() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Allow-Headers", "*") + c.Header("Access-Control-Allow-Methods", "*") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + } +} + +func CORSMiddleware(cfg config.HTTP) gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", cfg.FrontEndAdress) + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Header("Access-Control-Allow-Methods", "GET, POST, HEAD, PATCH, OPTIONS, PUT") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + // Swagger spec: // @title Token Factory API // @version 1.0 -// @BasePath /v1 +// @BasePath / func NewRouter( handler *gin.Engine, pKp, pHor, pEnv entity.ProducerInterface, @@ -27,21 +58,21 @@ func NewRouter( rolePermissionUc usecase.RolePermissionUseCase, vaultCategoryUc usecase.VaultCategoryUseCase, vaultUc usecase.VaultUseCase, + cfg config.HTTP, ) { - // Options + // Messenger + messengerController := newHTTPControllerMessenger(pKp, pHor, pEnv) + // Options Gin handler.Use(gin.Logger()) handler.Use(gin.Recovery()) - // Swagger docs.SwaggerInfo.BasePath = "/v1" handler.GET("v1/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - // K8s probe handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) - // Routers + handler.Use(CORSMiddleware(cfg)) // Alow only frontend origin groupV1 := handler.Group("/v1") - messengerController := newHTTPControllerMessenger(pKp, pHor, pEnv) { newUserRoutes(groupV1, userUseCase, authUseCase, rolePermissionUc) newWalletsRoutes(groupV1, walletUseCase, messengerController) @@ -51,4 +82,8 @@ func NewRouter( newVaultCategoryRoutes(groupV1, messengerController, authUseCase, vaultCategoryUc) newVaultRoutes(groupV1, messengerController, authUseCase, vaultUc, vaultCategoryUc, walletUseCase, assetUseCase) } + + // Toml Route + handler.Use(CORSMiddlewareAllowAllOrigins()) // Allow all origins for the toml + newAssetTomlRoutes(handler.Group("/"), walletUseCase, assetUseCase, messengerController, authUseCase) } diff --git a/backend/internal/controller/http/v1/vault.go b/backend/internal/controller/http/v1/vault.go index 83135cdd..4c34fbd9 100644 --- a/backend/internal/controller/http/v1/vault.go +++ b/backend/internal/controller/http/v1/vault.go @@ -20,7 +20,8 @@ type vaultRoutes struct { } func newVaultRoutes(handler *gin.RouterGroup, m HTTPControllerMessenger, a usecase.AuthUseCase, v usecase.VaultUseCase, vc usecase.VaultCategoryUseCase, - w usecase.WalletUseCase, as usecase.AssetUseCase) { + w usecase.WalletUseCase, as usecase.AssetUseCase, +) { r := &vaultRoutes{m, a, v, vc, w, as} h := handler.Group("/vault").Use(Auth(r.a.ValidateToken())) { @@ -33,7 +34,7 @@ func newVaultRoutes(handler *gin.RouterGroup, m HTTPControllerMessenger, a useca type CreateVaultRequest struct { Name string `json:"name" binding:"required" example:"Treasury"` VaultCategoryId int `json:"vault_category_id" binding:"required" example:"1"` - AssetsId []int `json:"assets_id" binding:"required" example:"[1]"` + AssetsId []int `json:"assets_id" binding:"required" example:"[11,2]"` } // @Summary Create a new vault @@ -41,7 +42,7 @@ type CreateVaultRequest struct { // @Tags Vault // @Accept json // @Produce json -// @Param request body CreateVaultRequest true "Vault info" +// @Param request body CreateVaultRequest true "CreateVaultRequest" // @Success 200 {object} entity.Vault // @Failure 400 {object} response // @Failure 404 {object} response diff --git a/backend/internal/entity/asset.go b/backend/internal/entity/asset.go index 843c215f..b8cd2ae6 100644 --- a/backend/internal/entity/asset.go +++ b/backend/internal/entity/asset.go @@ -89,3 +89,10 @@ type Validator struct { PublicKey string `toml:"PUBLIC_KEY"` History string `toml:"HISTORY"` } + +type Toml struct { + ID int + Content string + CreatedAt string `pg:"default:now()"` + UpdatedAt string `pg:"default:now()"` +} diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index dc303b6e..509f4eb2 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -10,14 +10,16 @@ import ( type AssetUseCase struct { aRepo AssetRepoInterface wRepo WalletRepoInterface - tRepo TomlInterface + tInt TomlInterface + tRepo TomlRepoInterface cfg config.Horizon } -func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface, tRepo TomlInterface, cfg config.Horizon) *AssetUseCase { +func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface, tInt TomlInterface, tRepo TomlRepoInterface, cfg config.Horizon) *AssetUseCase { return &AssetUseCase{ aRepo: aRepo, wRepo: wRepo, + tInt: tInt, tRepo: tRepo, cfg: cfg, } @@ -72,13 +74,147 @@ func (uc *AssetUseCase) GetAll() ([]entity.Asset, error) { } func (uc *AssetUseCase) CreateToml(req entity.TomlData) (string, error) { - toml, err := uc.tRepo.GenerateToml(req, uc.cfg) + toml, err := uc.tInt.GenerateToml(req, uc.cfg) + if err != nil { + return "", fmt.Errorf("AssetUseCase - CreateToml - uc.tInt.GenerateToml: %w", err) + } + + _, err = uc.tRepo.CreateToml(toml) + if err != nil { + return "", fmt.Errorf("AssetUseCase - CreateToml - uc.repo.CreateToml: %w", err) + } return toml, err } -func (uc *AssetUseCase) RetrieveToml(code string) (string, error) { - toml, err := uc.tRepo.RetrieveToml(code) +func (uc *AssetUseCase) RetrieveToml() (string, error) { + toml, err := uc.tRepo.GetToml() + if err != nil { + return "", fmt.Errorf("AssetUseCase - RetrieveToml - uc.repo.GetToml: %w", err) + } return toml, err } + +func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) { + // Get old toml data + oldTomlDb, err := uc.tRepo.GetToml() + if err != nil { + return "", fmt.Errorf("AssetUseCase - RetrieveToml - uc.repo.GetToml: %w", err) + } + + oldTomlData, err := uc.tInt.RetrieveToml(oldTomlDb) + if err != nil { + return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.tInt.GenerateToml: %w", err) + } + + // Update old toml data with new data + newTomlData := updateTomlData(oldTomlData, updatedToml) + + // Generate new toml in the database + tomlCreated, err := uc.tInt.GenerateToml(newTomlData, uc.cfg) + if err != nil { + return "", fmt.Errorf("AssetUseCase - CreateToml - uc.tInt.GenerateToml: %w", err) + } + + _, err = uc.tRepo.CreateToml(tomlCreated) + if err != nil { + return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.repo.CreateToml: %w", err) + } + + return tomlCreated, err +} + +func updateTomlData(existing, updated entity.TomlData) entity.TomlData { + if updated.Version != "" { + existing.Version = updated.Version + } + if updated.NetworkPassphrase != "" { + existing.NetworkPassphrase = updated.NetworkPassphrase + } + if updated.FederationServer != "" { + existing.FederationServer = updated.FederationServer + } + if updated.TransferServer != "" { + existing.TransferServer = updated.TransferServer + } + if updated.SigningKey != "" { + existing.SigningKey = updated.SigningKey + } + if updated.HorizonURL != "" { + existing.HorizonURL = updated.HorizonURL + } + // ... update other fields in a similar way + + existing.Accounts = appendIfNotExistsSlice(existing.Accounts, updated.Accounts) + existing.Principals = appendIfNotExistsPrincipalSlice(existing.Principals, updated.Principals) + existing.Currencies = appendIfNotExistsCurrencySlice(existing.Currencies, updated.Currencies) + existing.Validators = appendIfNotExistsValidatorSlice(existing.Validators, updated.Validators) + // ... update other slices in a similar way + + return existing +} + +func appendIfNotExistsSlice(existing []string, newItems []string) []string { + for _, newItem := range newItems { + found := false + for _, item := range existing { + if item == newItem { + found = true + break + } + } + if !found { + existing = append(existing, newItem) + } + } + return existing +} + +func appendIfNotExistsPrincipalSlice(existing []entity.Principal, newItems []entity.Principal) []entity.Principal { + for _, newItem := range newItems { + found := false + for _, item := range existing { + if item.Name == newItem.Name { + found = true + break + } + } + if !found { + existing = append(existing, newItem) + } + } + return existing +} + +func appendIfNotExistsCurrencySlice(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { + for _, newItem := range newItems { + found := false + for _, item := range existing { + if item.Code == newItem.Code { + found = true + break + } + } + if !found { + existing = append(existing, newItem) + } + } + return existing +} + +func appendIfNotExistsValidatorSlice(existing []entity.Validator, newItems []entity.Validator) []entity.Validator { + for _, newItem := range newItems { + found := false + for _, item := range existing { + if item.Alias == newItem.Alias { + found = true + break + } + } + if !found { + existing = append(existing, newItem) + } + } + return existing +} diff --git a/backend/internal/usecase/assets_test.go b/backend/internal/usecase/assets_test.go index c46a1dc5..426c2ed3 100644 --- a/backend/internal/usecase/assets_test.go +++ b/backend/internal/usecase/assets_test.go @@ -35,7 +35,8 @@ func asset(t *testing.T) (*usecase.AssetUseCase, *mocks.MockAssetRepoInterface, rw := mocks.NewMockWalletRepoInterface(mockCtl) ra := mocks.NewMockAssetRepoInterface(mockCtl) tg := mocks.NewMockTomlInterface(mockCtl) - u := usecase.NewAssetUseCase(ra, rw, tg, config.Horizon{}) + tr := mocks.NewMockTomlRepoInterface(mockCtl) + u := usecase.NewAssetUseCase(ra, rw, tg, tr, config.Horizon{}) return u, ra, rw, tg } diff --git a/backend/internal/usecase/interfaces.go b/backend/internal/usecase/interfaces.go index b1045225..e873bd30 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -60,7 +60,12 @@ type ( TomlInterface interface { GenerateToml(entity.TomlData, config.Horizon) (string, error) - RetrieveToml(string) (string, error) + RetrieveToml(string) (entity.TomlData, error) + } + + TomlRepoInterface interface { + CreateToml(string) (string, error) + GetToml() (string, error) } VaultCategoryRepoInterface interface { diff --git a/backend/internal/usecase/mocks/mocks.go b/backend/internal/usecase/mocks/mocks.go index ef0bb20e..59c7f85d 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -575,10 +575,10 @@ func (mr *MockTomlInterfaceMockRecorder) GenerateToml(arg0, arg1 interface{}) *g } // RetrieveToml mocks base method. -func (m *MockTomlInterface) RetrieveToml(arg0 string) (string, error) { +func (m *MockTomlInterface) RetrieveToml(arg0 string) (entity.TomlData, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RetrieveToml", arg0) - ret0, _ := ret[0].(string) + ret0, _ := ret[0].(entity.TomlData) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -589,6 +589,59 @@ func (mr *MockTomlInterfaceMockRecorder) RetrieveToml(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveToml", reflect.TypeOf((*MockTomlInterface)(nil).RetrieveToml), arg0) } +// MockTomlRepoInterface is a mock of TomlRepoInterface interface. +type MockTomlRepoInterface struct { + ctrl *gomock.Controller + recorder *MockTomlRepoInterfaceMockRecorder +} + +// MockTomlRepoInterfaceMockRecorder is the mock recorder for MockTomlRepoInterface. +type MockTomlRepoInterfaceMockRecorder struct { + mock *MockTomlRepoInterface +} + +// NewMockTomlRepoInterface creates a new mock instance. +func NewMockTomlRepoInterface(ctrl *gomock.Controller) *MockTomlRepoInterface { + mock := &MockTomlRepoInterface{ctrl: ctrl} + mock.recorder = &MockTomlRepoInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTomlRepoInterface) EXPECT() *MockTomlRepoInterfaceMockRecorder { + return m.recorder +} + +// CreateToml mocks base method. +func (m *MockTomlRepoInterface) CreateToml(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateToml", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateToml indicates an expected call of CreateToml. +func (mr *MockTomlRepoInterfaceMockRecorder) CreateToml(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateToml", reflect.TypeOf((*MockTomlRepoInterface)(nil).CreateToml), arg0) +} + +// GetToml mocks base method. +func (m *MockTomlRepoInterface) GetToml() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetToml") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetToml indicates an expected call of GetToml. +func (mr *MockTomlRepoInterfaceMockRecorder) GetToml() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToml", reflect.TypeOf((*MockTomlRepoInterface)(nil).GetToml)) +} + // MockVaultCategoryRepoInterface is a mock of VaultCategoryRepoInterface interface. type MockVaultCategoryRepoInterface struct { ctrl *gomock.Controller diff --git a/backend/internal/usecase/repo/toml_postgres.go b/backend/internal/usecase/repo/toml_postgres.go new file mode 100644 index 00000000..1ed3dc07 --- /dev/null +++ b/backend/internal/usecase/repo/toml_postgres.go @@ -0,0 +1,37 @@ +package repo + +import ( + "fmt" + + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/postgres" +) + +type TomlRepoInterface struct { + *postgres.Postgres +} + +func NewTomlRepo(pg *postgres.Postgres) TomlRepoInterface { + return TomlRepoInterface{ + Postgres: pg, + } +} + +func (r TomlRepoInterface) CreateToml(content string) (string, error) { + res := content + stmt := `INSERT INTO toml (content) VALUES ($1) RETURNING toml;` + err := r.Db.QueryRow(stmt, content).Scan(&res) + if err != nil { + return "", fmt.Errorf("AssetRepo - CreateToml - db.QueryRow: %w", err) + } + return res, nil +} + +func (r TomlRepoInterface) GetToml() (string, error) { + var res string + stmt := `SELECT content FROM toml ORDER BY id DESC LIMIT 1;` + err := r.Db.QueryRow(stmt).Scan(&res) + if err != nil { + return "", fmt.Errorf("AssetRepo - GetToml - db.QueryRow: %w", err) + } + return res, nil +} diff --git a/backend/internal/usecase/repo/wallet_postgres.go b/backend/internal/usecase/repo/wallet_postgres.go index 9b2f6005..7d13c67a 100644 --- a/backend/internal/usecase/repo/wallet_postgres.go +++ b/backend/internal/usecase/repo/wallet_postgres.go @@ -24,7 +24,6 @@ func (r WalletRepo) GetWallet(id int) (entity.Wallet, error) { var wallet entity.Wallet err := row.Scan(&wallet.Id, &wallet.Type, &wallet.Funded) - if err != nil { if err == sql.ErrNoRows { return entity.Wallet{}, fmt.Errorf("WalletRepo - GetWallet - wallet not found") @@ -36,29 +35,29 @@ func (r WalletRepo) GetWallet(id int) (entity.Wallet, error) { } func (r WalletRepo) GetWallets(wType string) ([]entity.Wallet, error) { - stmt := `SELECT * FROM Wallet WHERE type=$1` + stmt := `SELECT wallet.*, key.* FROM Wallet LEFT JOIN Key ON wallet.id = key.wallet_id WHERE wallet.type=$1` rows, err := r.Db.Query(stmt, wType) - if err != nil { return nil, fmt.Errorf("WalletRepo - GetWallets - db.Query: %w", err) } - defer rows.Close() - entities := make([]entity.Wallet, 0, _defaultEntityCap) + var wallets []entity.Wallet for rows.Next() { var wallet entity.Wallet + var key entity.Key - err = rows.Scan(&wallet.Id, &wallet.Type, &wallet.Funded) + err = rows.Scan(&wallet.Id, &wallet.Type, &wallet.Funded, &key.Id, &key.PublicKey, &key.Weight, &key.WalletId) if err != nil { return nil, fmt.Errorf("WalletRepo - GetWallets - rows.Scan: %w", err) } - entities = append(entities, wallet) + wallet.Key = key + wallets = append(wallets, wallet) } - return entities, nil + return wallets, nil } func (r WalletRepo) GetKeyByWallet(walletId int) (entity.Key, error) { @@ -67,7 +66,6 @@ func (r WalletRepo) GetKeyByWallet(walletId int) (entity.Key, error) { var key entity.Key err := row.Scan(&key.Id, &key.PublicKey, &key.Weight, &key.WalletId) - if err != nil { if err == sql.ErrNoRows { return entity.Key{}, fmt.Errorf("WalletRepo - GetKeyByWallet - key not found") @@ -82,7 +80,6 @@ func (r WalletRepo) CreateWallet(data entity.Wallet) (entity.Wallet, error) { res := data stmt := `INSERT INTO Wallet (type) VALUES ($1) RETURNING id;` err := r.Db.QueryRow(stmt, data.Type).Scan(&res.Id) - if err != nil { return entity.Wallet{}, fmt.Errorf("WalletRepo - CreateWallet - db.QueryRow: %w", err) } @@ -94,7 +91,6 @@ func (r WalletRepo) CreateKey(data entity.Key) (entity.Key, error) { res := data stmt := `INSERT INTO Key (public_key, weight, wallet_id) VALUES ($1, $2, $3) RETURNING id;` err := r.Db.QueryRow(stmt, data.PublicKey, data.Weight, data.WalletId).Scan(&res.Id) - if err != nil { return entity.Key{}, fmt.Errorf("WalletRepo - CreateKey - db.QueryRow: %w", err) } @@ -114,13 +110,11 @@ func (r WalletRepo) CreateWalletWithKey(data entity.Wallet) (entity.Wallet, erro return entity.Wallet{}, fmt.Errorf("WalletRepo - CreateWalletWithKey - uc.repo.CreateKey: %w", err) } return wallet, nil - } func (r WalletRepo) UpdateWallet(data entity.Wallet) (entity.Wallet, error) { stmt := `UPDATE Wallet SET funded=($1) WHERE id=($2);` result, err := r.Db.Exec(stmt, data.Funded, data.Id) - if err != nil { return entity.Wallet{}, fmt.Errorf("WalletRepo - UpdateWallet - db.Exec: %v", err) } diff --git a/backend/internal/usecase/wallets.go b/backend/internal/usecase/wallets.go index 3929ad2f..de9dce9c 100644 --- a/backend/internal/usecase/wallets.go +++ b/backend/internal/usecase/wallets.go @@ -53,3 +53,11 @@ func (uc *WalletUseCase) Update(data entity.Wallet) (entity.Wallet, error) { } return wallet, nil } + +func (uc *WalletUseCase) GetWalletsByType(wType string) ([]entity.Wallet, error) { + wallets, err := uc.repo.GetWallets(wType) + if err != nil { + return nil, fmt.Errorf("WalletUseCase - GetAll - uc.repo.GetAllWallets: %w", err) + } + return wallets, nil +} diff --git a/backend/pkg/toml/toml.go b/backend/pkg/toml/toml.go index 5e31fe93..688ca821 100644 --- a/backend/pkg/toml/toml.go +++ b/backend/pkg/toml/toml.go @@ -26,6 +26,11 @@ func (g *DefaultTomlGenerator) GenerateToml(req entity.TomlData, cfg config.Hori return string(tomlBytes), nil } -func (g *DefaultTomlGenerator) RetrieveToml(asset string) (string, error) { - return "", nil +func (g *DefaultTomlGenerator) RetrieveToml(data string) (entity.TomlData, error) { + var cfg entity.TomlData + err := toml.Unmarshal([]byte(data), &cfg) + if err != nil { + return entity.TomlData{}, err + } + return cfg, nil } From f1c58c4a06d0442d313d196b83dfc914e7ce436e Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 13:41:07 -0300 Subject: [PATCH 06/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Create=20a=20new=20G?= =?UTF-8?q?eneric=20function=20to=20parse=20the=20itens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/usecase/assets.go | 81 ++++++++++-------------------- 1 file changed, 26 insertions(+), 55 deletions(-) diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index 509f4eb2..d5c930ad 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -97,26 +97,27 @@ func (uc *AssetUseCase) RetrieveToml() (string, error) { } func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) { - // Get old toml data + // Get old toml data in the database oldTomlDb, err := uc.tRepo.GetToml() if err != nil { return "", fmt.Errorf("AssetUseCase - RetrieveToml - uc.repo.GetToml: %w", err) } - oldTomlData, err := uc.tInt.RetrieveToml(oldTomlDb) + oldTomParsed, err := uc.tInt.RetrieveToml(oldTomlDb) // Parse old toml data if err != nil { return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.tInt.GenerateToml: %w", err) } // Update old toml data with new data - newTomlData := updateTomlData(oldTomlData, updatedToml) + newTomlParsed := updateTomlData(oldTomParsed, updatedToml) - // Generate new toml in the database - tomlCreated, err := uc.tInt.GenerateToml(newTomlData, uc.cfg) + // Parse the new toml data in a TOML string + tomlCreated, err := uc.tInt.GenerateToml(newTomlParsed, uc.cfg) if err != nil { return "", fmt.Errorf("AssetUseCase - CreateToml - uc.tInt.GenerateToml: %w", err) } + // Save the new toml data in the database _, err = uc.tRepo.CreateToml(tomlCreated) if err != nil { return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.repo.CreateToml: %w", err) @@ -144,22 +145,22 @@ func updateTomlData(existing, updated entity.TomlData) entity.TomlData { if updated.HorizonURL != "" { existing.HorizonURL = updated.HorizonURL } - // ... update other fields in a similar way - existing.Accounts = appendIfNotExistsSlice(existing.Accounts, updated.Accounts) - existing.Principals = appendIfNotExistsPrincipalSlice(existing.Principals, updated.Principals) - existing.Currencies = appendIfNotExistsCurrencySlice(existing.Currencies, updated.Currencies) - existing.Validators = appendIfNotExistsValidatorSlice(existing.Validators, updated.Validators) - // ... update other slices in a similar way + existing.Accounts = appendIfNotExists(existing.Accounts, updated.Accounts, func(item1, item2 string) bool { + return item1 == item2 + }) + existing.Principals = appendIfNotExistsPrincipal(existing.Principals, updated.Principals) + existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) + existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) return existing } -func appendIfNotExistsSlice(existing []string, newItems []string) []string { +func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { for _, newItem := range newItems { found := false for _, item := range existing { - if item == newItem { + if equals(item, newItem) { found = true break } @@ -171,50 +172,20 @@ func appendIfNotExistsSlice(existing []string, newItems []string) []string { return existing } -func appendIfNotExistsPrincipalSlice(existing []entity.Principal, newItems []entity.Principal) []entity.Principal { - for _, newItem := range newItems { - found := false - for _, item := range existing { - if item.Name == newItem.Name { - found = true - break - } - } - if !found { - existing = append(existing, newItem) - } - } - return existing +func appendIfNotExistsPrincipal(existing []entity.Principal, newItems []entity.Principal) []entity.Principal { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Principal) bool { + return item1.Name == item2.Name + }) } -func appendIfNotExistsCurrencySlice(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { - for _, newItem := range newItems { - found := false - for _, item := range existing { - if item.Code == newItem.Code { - found = true - break - } - } - if !found { - existing = append(existing, newItem) - } - } - return existing +func appendIfNotExistsCurrency(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Currency) bool { + return item1.Code == item2.Code + }) } -func appendIfNotExistsValidatorSlice(existing []entity.Validator, newItems []entity.Validator) []entity.Validator { - for _, newItem := range newItems { - found := false - for _, item := range existing { - if item.Alias == newItem.Alias { - found = true - break - } - } - if !found { - existing = append(existing, newItem) - } - } - return existing +func appendIfNotExistsValidator(existing []entity.Validator, newItems []entity.Validator) []entity.Validator { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Validator) bool { + return item1.Alias == item2.Alias + }) } From ad4c4a24fd8fbbeed2cfd2b329d5fc259c1cf8f8 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 14:01:04 -0300 Subject: [PATCH 07/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Add=20a=20toml=20?= =?UTF-8?q?helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/usecase/assets.go | 69 ++----------------------- backend/internal/usecase/interfaces.go | 1 + backend/internal/usecase/mocks/mocks.go | 15 ++++++ backend/pkg/toml/toml_helpers.go | 67 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 backend/pkg/toml/toml_helpers.go diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index d5c930ad..0b8a4585 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -109,7 +109,10 @@ func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) } // Update old toml data with new data - newTomlParsed := updateTomlData(oldTomParsed, updatedToml) + newTomlParsed, err := uc.tInt.AppendTomlData(oldTomParsed, updatedToml) + if err != nil { + return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.tInt.GenerateToml: %w", err) + } // Parse the new toml data in a TOML string tomlCreated, err := uc.tInt.GenerateToml(newTomlParsed, uc.cfg) @@ -125,67 +128,3 @@ func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) return tomlCreated, err } - -func updateTomlData(existing, updated entity.TomlData) entity.TomlData { - if updated.Version != "" { - existing.Version = updated.Version - } - if updated.NetworkPassphrase != "" { - existing.NetworkPassphrase = updated.NetworkPassphrase - } - if updated.FederationServer != "" { - existing.FederationServer = updated.FederationServer - } - if updated.TransferServer != "" { - existing.TransferServer = updated.TransferServer - } - if updated.SigningKey != "" { - existing.SigningKey = updated.SigningKey - } - if updated.HorizonURL != "" { - existing.HorizonURL = updated.HorizonURL - } - - existing.Accounts = appendIfNotExists(existing.Accounts, updated.Accounts, func(item1, item2 string) bool { - return item1 == item2 - }) - existing.Principals = appendIfNotExistsPrincipal(existing.Principals, updated.Principals) - existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) - existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) - - return existing -} - -func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { - for _, newItem := range newItems { - found := false - for _, item := range existing { - if equals(item, newItem) { - found = true - break - } - } - if !found { - existing = append(existing, newItem) - } - } - return existing -} - -func appendIfNotExistsPrincipal(existing []entity.Principal, newItems []entity.Principal) []entity.Principal { - return appendIfNotExists(existing, newItems, func(item1, item2 entity.Principal) bool { - return item1.Name == item2.Name - }) -} - -func appendIfNotExistsCurrency(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { - return appendIfNotExists(existing, newItems, func(item1, item2 entity.Currency) bool { - return item1.Code == item2.Code - }) -} - -func appendIfNotExistsValidator(existing []entity.Validator, newItems []entity.Validator) []entity.Validator { - return appendIfNotExists(existing, newItems, func(item1, item2 entity.Validator) bool { - return item1.Alias == item2.Alias - }) -} diff --git a/backend/internal/usecase/interfaces.go b/backend/internal/usecase/interfaces.go index e873bd30..0b4901fa 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -61,6 +61,7 @@ type ( TomlInterface interface { GenerateToml(entity.TomlData, config.Horizon) (string, error) RetrieveToml(string) (entity.TomlData, error) + AppendTomlData(entity.TomlData, entity.TomlData) (entity.TomlData, error) } TomlRepoInterface interface { diff --git a/backend/internal/usecase/mocks/mocks.go b/backend/internal/usecase/mocks/mocks.go index 59c7f85d..9d3b041b 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -559,6 +559,21 @@ func (m *MockTomlInterface) EXPECT() *MockTomlInterfaceMockRecorder { return m.recorder } +// AppendTomlData mocks base method. +func (m *MockTomlInterface) AppendTomlData(arg0, arg1 entity.TomlData) (entity.TomlData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AppendTomlData", arg0, arg1) + ret0, _ := ret[0].(entity.TomlData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AppendTomlData indicates an expected call of AppendTomlData. +func (mr *MockTomlInterfaceMockRecorder) AppendTomlData(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendTomlData", reflect.TypeOf((*MockTomlInterface)(nil).AppendTomlData), arg0, arg1) +} + // GenerateToml mocks base method. func (m *MockTomlInterface) GenerateToml(arg0 entity.TomlData, arg1 config.Horizon) (string, error) { m.ctrl.T.Helper() diff --git a/backend/pkg/toml/toml_helpers.go b/backend/pkg/toml/toml_helpers.go new file mode 100644 index 00000000..601e4cc0 --- /dev/null +++ b/backend/pkg/toml/toml_helpers.go @@ -0,0 +1,67 @@ +package toml + +import "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" + +func AppendTomlData(existing, updated entity.TomlData) entity.TomlData { + if updated.Version != "" { + existing.Version = updated.Version + } + if updated.NetworkPassphrase != "" { + existing.NetworkPassphrase = updated.NetworkPassphrase + } + if updated.FederationServer != "" { + existing.FederationServer = updated.FederationServer + } + if updated.TransferServer != "" { + existing.TransferServer = updated.TransferServer + } + if updated.SigningKey != "" { + existing.SigningKey = updated.SigningKey + } + if updated.HorizonURL != "" { + existing.HorizonURL = updated.HorizonURL + } + + existing.Accounts = appendIfNotExists(existing.Accounts, updated.Accounts, func(item1, item2 string) bool { + return item1 == item2 + }) + existing.Principals = appendIfNotExistsPrincipal(existing.Principals, updated.Principals) + existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) + existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) + + return existing +} + +func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { + for _, newItem := range newItems { + found := false + for _, item := range existing { + if equals(item, newItem) { + found = true + break + } + } + if !found { + existing = append(existing, newItem) + } + } + return existing +} + +func appendIfNotExistsPrincipal(existing []entity.Principal, newItems []entity.Principal) []entity.Principal { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Principal) bool { + return item1.Name == item2.Name + }) +} + +func appendIfNotExistsCurrency(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Currency) bool { + return item1.Code == item2.Code + }) +} + +func appendIfNotExistsValidator(existing []entity.Validator, newItems []entity.Validator) []entity.Validator { + return appendIfNotExists(existing, newItems, func(item1, item2 entity.Validator) bool { + return item1.Alias == item2.Alias + }) +} From 3f5c87f7dae401bc6a641394053c60e6c12e0094 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Wed, 9 Aug 2023 15:26:58 -0300 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=90=9B=20Add=20TomlGenerator=20in?= =?UTF-8?q?=20the=20TOML=20Helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pkg/toml/toml_helpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pkg/toml/toml_helpers.go b/backend/pkg/toml/toml_helpers.go index 601e4cc0..a7bd9138 100644 --- a/backend/pkg/toml/toml_helpers.go +++ b/backend/pkg/toml/toml_helpers.go @@ -2,7 +2,7 @@ package toml import "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" -func AppendTomlData(existing, updated entity.TomlData) entity.TomlData { +func (g *DefaultTomlGenerator) AppendTomlData(existing, updated entity.TomlData) (entity.TomlData, error) { if updated.Version != "" { existing.Version = updated.Version } @@ -29,7 +29,7 @@ func AppendTomlData(existing, updated entity.TomlData) entity.TomlData { existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) - return existing + return existing, nil } func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { From f67166e099ee77dd7a8644355999eb7381b09eeb Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Thu, 10 Aug 2023 18:53:41 -0300 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=85=20=20Update=20the=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 32 ++++++++++++++++++++++++++++++-- backend/config/config.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index aec1b969..1c4a6495 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,7 +18,7 @@ KAFKA_SCHEMA_REGISTRY_URL= # HTTP HTTP_PORT= -HTTP_FRONT_ADRESS= +HTTP_FRONT_ADRESS= ## JWT SECRET JWT_SECRET_KEY=secret @@ -30,4 +30,32 @@ HORIZON_PUBLIC_API_SERVER=https://horizon-testnet.stellar.org HORIZON_TEST_API_SERVER=https://horizon-testnet.stellar.org PUBLIC_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 TEST_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 -STELLAR_TOML_VERSION=2.5.0 \ No newline at end of file +STELLAR_TOML_VERSION=2.5.0 +HORIZON_URL= # Add your Horizon URL here +FEDERATION_SERVER= # Add your Federation Server here +TRANSFER_SERVER= # Add your Transfer Server here + +# Documentation +ORG_NAME= +ORG_DBA= +ORG_URL= +ORG_LOGO= +ORG_DESCRIPTION= +ORG_PHYSICAL_ADDRESS= +ORG_PHYSICAL_ADDRESS_ATTESTATION= +ORG_PHONE_NUMBER= +ORG_PHONE_NUMBER_ATTESTATION= +ORG_KEYBASE= +ORG_TWITTER= +ORG_GITHUB= +ORG_OFFICIAL_EMAIL= + +# PRINCIPALS +PRINCIPALS_NAME= +PRINCIPALS_EMAIL= +PRINCIPALS_KEYBASE= +PRINCIPALS_TWITTER= +PRINCIPALS_GITHUB= +PRINCIPALS_ID_PHOTO_HASH= +PRINCIPALS_VERIFICATION_PHOTO_HASH= + diff --git a/backend/config/config.go b/backend/config/config.go index aa03f0b7..3172dd60 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -57,6 +57,37 @@ type ( PublicNetworkPass string `env:"PUBLIC_NETWORK_PASSPHRASE"` TestNetworkPass string `env:"TEST_NETWORK_PASSPHRASE"` StellarTomlVersion string `env:"STELLAR_TOML_VERSION"` + HorizonURL string `env:"HORIZON_URL"` + FederationServer string `env:"FEDERATION_SERVER"` + TransferServer string `env:"TRANSFER_SERVER"` + Documentation Documentation + Principals Principals + } + + Documentation struct { + OrgName string `env:"ORG_NAME"` + OrgDBA string `env:"ORG_DBA"` + OrgURL string `env:"ORG_URL"` + OrgLogo string `env:"ORG_LOGO"` + OrgDescription string `env:"ORG_DESCRIPTION"` + OrgPhysicalAddress string `env:"ORG_PHYSICAL_ADDRESS"` + OrgPhysicalAddressAttestation string `env:"ORG_PHYSICAL_ADDRESS_ATTESTATION"` + OrgPhoneNumber string `env:"ORG_PHONE_NUMBER"` + OrgPhoneNumberAttestation string `env:"ORG_PHONE_NUMBER_ATTESTATION"` + OrgKeybase string `env:"ORG_KEYBASE"` + OrgTwitter string `env:"ORG_TWITTER"` + OrgGithub string `env:"ORG_GITHUB"` + OrgOfficialEmail string `env:"ORG_OFFICIAL_EMAIL"` + } + + Principals struct { + Name string `env:"PRINCIPALS_NAME"` + Email string `env:"PRINCIPALS_EMAIL"` + Keybase string `env:"PRINCIPALS_KEYBASE"` + Github string `env:"PRINCIPALS_GITHUB"` + Twitter string `env:"PRINCIPALS_TWITTER"` + IDPhotoHash string `env:"PRINCIPALS_ID_PHOTO_HASH"` + VerificationPhotoHash string `env:"PRINCIPALS_VERIFICATION_PHOTO_HASH"` } ) From 6691877b97ed98313b97059ff7752f2dd88cbc55 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Thu, 10 Aug 2023 18:57:14 -0300 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20the=20TOML=20uti?= =?UTF-8?q?ls,=20and=20add=20the=20updateTOML=20Route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/controller/http/v1/assets.go | 27 ++++++++ backend/internal/usecase/assets.go | 2 +- backend/internal/usecase/interfaces.go | 2 +- backend/pkg/toml/toml.go | 61 +++++++++++++++++-- backend/pkg/toml/toml_helpers.go | 41 ++++--------- 5 files changed, 99 insertions(+), 34 deletions(-) diff --git a/backend/internal/controller/http/v1/assets.go b/backend/internal/controller/http/v1/assets.go index 040b6768..68d6a2e2 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -41,6 +41,7 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/burn", r.burnAsset) h.POST("/transfer", r.transferAsset) h.POST("/generate-toml", r.generateTOML) + h.PUT("/update-toml", r.updateTOML) } } @@ -649,3 +650,29 @@ func (r *assetsRoutes) retrieveToml(c *gin.Context) { c.Data(http.StatusOK, "text/plain", []byte(tomlContent)) } + +// @Summary Update a TOML file +// @Description Update a TOML file +// @Tags Assets +// @Accept json +// @Produce json +// @Param request body entity.TomlData true "TOML info" +// @Success 200 {object} entity.TomlData +// @Failure 400 {object} response +// @Failure 500 {object} response +// @Router /assets/update-toml [put] +func (r *assetsRoutes) updateTOML(c *gin.Context) { + var request entity.TomlData + if err := c.ShouldBindJSON(&request); err != nil { + errorResponse(c, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error())) + return + } + + toml, err := r.as.UpdateToml(request) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error updating TOML") + return + } + + c.Data(http.StatusOK, "application/toml", []byte(toml)) +} diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index 0b8a4585..1782a2cd 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -109,7 +109,7 @@ func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) } // Update old toml data with new data - newTomlParsed, err := uc.tInt.AppendTomlData(oldTomParsed, updatedToml) + newTomlParsed, err := uc.tInt.UpdateTomlData(oldTomParsed, updatedToml) if err != nil { return "", fmt.Errorf("AssetUseCase - UpdateToml - uc.tInt.GenerateToml: %w", err) } diff --git a/backend/internal/usecase/interfaces.go b/backend/internal/usecase/interfaces.go index 0b4901fa..517dee1f 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -61,7 +61,7 @@ type ( TomlInterface interface { GenerateToml(entity.TomlData, config.Horizon) (string, error) RetrieveToml(string) (entity.TomlData, error) - AppendTomlData(entity.TomlData, entity.TomlData) (entity.TomlData, error) + UpdateTomlData(entity.TomlData, entity.TomlData) (entity.TomlData, error) } TomlRepoInterface interface { diff --git a/backend/pkg/toml/toml.go b/backend/pkg/toml/toml.go index 688ca821..0b170bbe 100644 --- a/backend/pkg/toml/toml.go +++ b/backend/pkg/toml/toml.go @@ -13,12 +13,44 @@ func NewTomlGenerator() *DefaultTomlGenerator { } func (g *DefaultTomlGenerator) GenerateToml(req entity.TomlData, cfg config.Horizon) (string, error) { - if req.Version == "" { - req.Version = cfg.StellarTomlVersion + fieldConfigs := []FieldConfig{ + {&req.Version, cfg.StellarTomlVersion}, + {&req.NetworkPassphrase, cfg.TestNetworkPass}, + {&req.FederationServer, cfg.FederationServer}, + {&req.TransferServer, cfg.TransferServer}, + {&req.HorizonURL, cfg.HorizonURL}, } - if req.NetworkPassphrase == "" { - req.NetworkPassphrase = cfg.TestNetworkPass + + docFieldConfigs := []FieldConfig{ + {&req.Documentation.OrgName, cfg.Documentation.OrgName}, + {&req.Documentation.OrgDBA, cfg.Documentation.OrgDBA}, + {&req.Documentation.OrgURL, cfg.Documentation.OrgURL}, + {&req.Documentation.OrgLogo, cfg.Documentation.OrgLogo}, + {&req.Documentation.OrgDescription, cfg.Documentation.OrgDescription}, + {&req.Documentation.OrgPhysicalAddress, cfg.Documentation.OrgPhysicalAddress}, + {&req.Documentation.OrgPhysicalAddressAttestation, cfg.Documentation.OrgPhysicalAddressAttestation}, + {&req.Documentation.OrgPhoneNumber, cfg.Documentation.OrgPhoneNumber}, + {&req.Documentation.OrgPhoneNumberAttestation, cfg.Documentation.OrgPhoneNumberAttestation}, + {&req.Documentation.OrgKeybase, cfg.Documentation.OrgKeybase}, + {&req.Documentation.OrgTwitter, cfg.Documentation.OrgTwitter}, + {&req.Documentation.OrgGithub, cfg.Documentation.OrgGithub}, + {&req.Documentation.OrgOfficialEmail, cfg.Documentation.OrgOfficialEmail}, + } + + principalFieldConfigs := []FieldConfig{ + {&req.Principals[0].Name, cfg.Principals.Name}, + {&req.Principals[0].Email, cfg.Principals.Email}, + {&req.Principals[0].Keybase, cfg.Principals.Keybase}, + {&req.Principals[0].Twitter, cfg.Principals.Twitter}, + {&req.Principals[0].Github, cfg.Principals.Github}, + {&req.Principals[0].IDPhotoHash, cfg.Principals.IDPhotoHash}, + {&req.Principals[0].VerificationPhotoHash, cfg.Principals.VerificationPhotoHash}, } + + updateFieldsIfEmpty(fieldConfigs) + updateFieldsIfEmpty(docFieldConfigs) + updateFieldsIfEmpty(principalFieldConfigs) + tomlBytes, err := toml.Marshal(req) if err != nil { return "", err @@ -26,6 +58,27 @@ func (g *DefaultTomlGenerator) GenerateToml(req entity.TomlData, cfg config.Hori return string(tomlBytes), nil } +func (g *DefaultTomlGenerator) UpdateTomlData(existing, updated entity.TomlData) (entity.TomlData, error) { + updateFieldsIfEmpty([]FieldConfig{ + {&existing.Version, updated.Version}, + {&existing.NetworkPassphrase, updated.NetworkPassphrase}, + {&existing.FederationServer, updated.FederationServer}, + {&existing.TransferServer, updated.TransferServer}, + {&existing.SigningKey, updated.SigningKey}, + {&existing.HorizonURL, updated.HorizonURL}, + }) + + existing.Documentation = updated.Documentation + existing.Accounts = appendIfNotExists(existing.Accounts, updated.Accounts, func(item1, item2 string) bool { + return item1 == item2 + }) + existing.Principals = appendIfNotExistsPrincipal(existing.Principals, updated.Principals) + existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) + existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) + + return existing, nil +} + func (g *DefaultTomlGenerator) RetrieveToml(data string) (entity.TomlData, error) { var cfg entity.TomlData err := toml.Unmarshal([]byte(data), &cfg) diff --git a/backend/pkg/toml/toml_helpers.go b/backend/pkg/toml/toml_helpers.go index a7bd9138..c9cd687b 100644 --- a/backend/pkg/toml/toml_helpers.go +++ b/backend/pkg/toml/toml_helpers.go @@ -1,35 +1,20 @@ package toml -import "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" +) -func (g *DefaultTomlGenerator) AppendTomlData(existing, updated entity.TomlData) (entity.TomlData, error) { - if updated.Version != "" { - existing.Version = updated.Version - } - if updated.NetworkPassphrase != "" { - existing.NetworkPassphrase = updated.NetworkPassphrase - } - if updated.FederationServer != "" { - existing.FederationServer = updated.FederationServer - } - if updated.TransferServer != "" { - existing.TransferServer = updated.TransferServer - } - if updated.SigningKey != "" { - existing.SigningKey = updated.SigningKey - } - if updated.HorizonURL != "" { - existing.HorizonURL = updated.HorizonURL - } - - existing.Accounts = appendIfNotExists(existing.Accounts, updated.Accounts, func(item1, item2 string) bool { - return item1 == item2 - }) - existing.Principals = appendIfNotExistsPrincipal(existing.Principals, updated.Principals) - existing.Currencies = appendIfNotExistsCurrency(existing.Currencies, updated.Currencies) - existing.Validators = appendIfNotExistsValidator(existing.Validators, updated.Validators) +type FieldConfig struct { + Field *string + ConfigVal string +} - return existing, nil +func updateFieldsIfEmpty(fields []FieldConfig) { + for _, field := range fields { + if *field.Field == "" { + *field.Field = field.ConfigVal + } + } } func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { From 0165cf81804bad76533e7bdf047567c7e0edca78 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Thu, 10 Aug 2023 18:58:00 -0300 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=A4=A1=20Mock=20the=20new=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/usecase/mocks/mocks.go | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/internal/usecase/mocks/mocks.go b/backend/internal/usecase/mocks/mocks.go index 9d3b041b..eef75b0d 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -559,21 +559,6 @@ func (m *MockTomlInterface) EXPECT() *MockTomlInterfaceMockRecorder { return m.recorder } -// AppendTomlData mocks base method. -func (m *MockTomlInterface) AppendTomlData(arg0, arg1 entity.TomlData) (entity.TomlData, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AppendTomlData", arg0, arg1) - ret0, _ := ret[0].(entity.TomlData) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AppendTomlData indicates an expected call of AppendTomlData. -func (mr *MockTomlInterfaceMockRecorder) AppendTomlData(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendTomlData", reflect.TypeOf((*MockTomlInterface)(nil).AppendTomlData), arg0, arg1) -} - // GenerateToml mocks base method. func (m *MockTomlInterface) GenerateToml(arg0 entity.TomlData, arg1 config.Horizon) (string, error) { m.ctrl.T.Helper() @@ -604,6 +589,21 @@ func (mr *MockTomlInterfaceMockRecorder) RetrieveToml(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveToml", reflect.TypeOf((*MockTomlInterface)(nil).RetrieveToml), arg0) } +// UpdateTomlData mocks base method. +func (m *MockTomlInterface) UpdateTomlData(arg0, arg1 entity.TomlData) (entity.TomlData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTomlData", arg0, arg1) + ret0, _ := ret[0].(entity.TomlData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateTomlData indicates an expected call of UpdateTomlData. +func (mr *MockTomlInterfaceMockRecorder) UpdateTomlData(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTomlData", reflect.TypeOf((*MockTomlInterface)(nil).UpdateTomlData), arg0, arg1) +} + // MockTomlRepoInterface is a mock of TomlRepoInterface interface. type MockTomlRepoInterface struct { ctrl *gomock.Controller From 90a1ba207dd9d4b219e092cef5a37371f29bd942 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Tue, 12 Sep 2023 10:11:50 -0300 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20the=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/controller/http/v1/assets.go | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/internal/controller/http/v1/assets.go b/backend/internal/controller/http/v1/assets.go index 96e1777e..feecb86d 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -54,16 +54,14 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca } type CreateAssetRequest struct { - SponsorId int `json:"sponsor_id" example:"2"` - Name string `json:"name" binding:"required" example:"USDC"` - AssetType string `json:"asset_type" binding:"required" example:"security_token"` - Code string `json:"code" binding:"required" example:"USDC"` - Limit *int `json:"limit" example:"1000"` - Amount string `json:"amount" example:"1000"` - SetFlags []string `json:"set_flags" example:"[\"AUTH_REQUIRED_FLAGS\", \"AUTH_REVOCABLE_FLAGS\",\"AUTH_CLAWBACK_ENABLED\"]"` - Image string `json:"image" example:"iVBORw0KGgoAAAANSUhEUgAACqoAAAMMCAMAAAAWqpRaAAADAFBMVEX///..."` - SetToml bool `json:"set_toml" example:"true"` - TomlData entity.TomlData `json:"toml_data" example:"Example entity.TomlData"` + SponsorId int `json:"sponsor_id" example:"2"` + Name string `json:"name" binding:"required" example:"USDC"` + AssetType string `json:"asset_type" binding:"required" example:"security_token"` + Code string `json:"code" binding:"required" example:"USDC"` + Limit *int `json:"limit" example:"1000"` + Amount string `json:"amount" example:"1000"` + SetFlags []string `json:"set_flags" example:"[\"AUTH_REQUIRED_FLAGS\", \"AUTH_REVOCABLE_FLAGS\",\"AUTH_CLAWBACK_ENABLED\"]"` + Image string `json:"image" example:"iVBORw0KGgoAAAANSUhEUgAACqoAAAMMCAMAAAAWqpRaAAADAFBMVEX///..."` } type BurnAssetRequest struct { @@ -278,19 +276,25 @@ func (r *assetsRoutes) createAsset(c *gin.Context) { return } - if !request.SetToml { - c.JSON(http.StatusOK, asset) + // Set TOML data + var tomlData entity.TomlData + tomlData.Currencies = []entity.Currency{ + { + Code: asset.Code, + Issuer: asset.Issuer.Key.PublicKey, + Name: asset.Name, + }, } if asset.Id == 1 { - _, err = r.as.CreateToml(request.TomlData) + _, err = r.as.CreateToml(tomlData) if err != nil { errorResponse(c, http.StatusNotFound, "error to create TOML ", err) return } } - _, err = r.as.UpdateToml(request.TomlData) + _, err = r.as.UpdateToml(tomlData) if err != nil { errorResponse(c, http.StatusNotFound, "error to update TOML ", err) return From 8aa2f87168118b923d32245bbdcd3fc2faf665d9 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Thu, 28 Sep 2023 17:33:51 -0300 Subject: [PATCH 13/16] FIX Migrations --- backend/migrations/000010_role_permission_juntion.up.sql | 2 +- backend/migrations/000019_asset_image.down.sql | 1 - backend/migrations/000019_asset_image.up.sql | 1 - .../migrations/{000020_toml.down.sql => 000027_toml.down.sql} | 0 backend/migrations/{000020_toml.up.sql => 000027_toml.up.sql} | 0 5 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 backend/migrations/000019_asset_image.down.sql delete mode 100644 backend/migrations/000019_asset_image.up.sql rename backend/migrations/{000020_toml.down.sql => 000027_toml.down.sql} (100%) rename backend/migrations/{000020_toml.up.sql => 000027_toml.up.sql} (100%) diff --git a/backend/migrations/000010_role_permission_juntion.up.sql b/backend/migrations/000010_role_permission_juntion.up.sql index dcfcaa28..7b21f014 100644 --- a/backend/migrations/000010_role_permission_juntion.up.sql +++ b/backend/migrations/000010_role_permission_juntion.up.sql @@ -5,7 +5,7 @@ CREATE TABLE RolePermissionJunction ( CONSTRAINT FK_role FOREIGN KEY (role_id) REFERENCES Role (id), CONSTRAINT FK_permission - FOREIGN KEY (permission_id) REFERENCES Permission (id) + FOREIGN KEY (permission_id) REFERENCES Permission (id) ON DELETE CASCADE ); insert into rolepermissionjunction (role_id, permission_id) values (1, 1); diff --git a/backend/migrations/000019_asset_image.down.sql b/backend/migrations/000019_asset_image.down.sql deleted file mode 100644 index cb93810c..00000000 --- a/backend/migrations/000019_asset_image.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE asset DROP COLUMN image; \ No newline at end of file diff --git a/backend/migrations/000019_asset_image.up.sql b/backend/migrations/000019_asset_image.up.sql deleted file mode 100644 index a4c5da83..00000000 --- a/backend/migrations/000019_asset_image.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE asset ADD COLUMN image BYTEA; \ No newline at end of file diff --git a/backend/migrations/000020_toml.down.sql b/backend/migrations/000027_toml.down.sql similarity index 100% rename from backend/migrations/000020_toml.down.sql rename to backend/migrations/000027_toml.down.sql diff --git a/backend/migrations/000020_toml.up.sql b/backend/migrations/000027_toml.up.sql similarity index 100% rename from backend/migrations/000020_toml.up.sql rename to backend/migrations/000027_toml.up.sql From 80f58f82a75d7e65e1ca5f530e24c39cde4e40c7 Mon Sep 17 00:00:00 2001 From: Wellington Junior Date: Thu, 28 Sep 2023 17:44:29 -0300 Subject: [PATCH 14/16] Fix: Fix the lint --- backend/integration-test/integration_test.go | 11 +++-------- backend/internal/controller/http/v1/utils.go | 5 ++++- backend/internal/usecase/role_permission_test.go | 10 ---------- backend/internal/usecase/users.go | 9 ++++++--- backend/internal/usecase/vault_test.go | 3 --- backend/pkg/kafka/connection.go | 6 +++++- 6 files changed, 18 insertions(+), 26 deletions(-) diff --git a/backend/integration-test/integration_test.go b/backend/integration-test/integration_test.go index 691dc5d2..87847d20 100644 --- a/backend/integration-test/integration_test.go +++ b/backend/integration-test/integration_test.go @@ -13,16 +13,11 @@ import ( const ( // Attempts connection host = "backend:8080" - healthPath = "http://" + host + "/healthz" + healthPath = "/healthz" attempts = 20 // HTTP REST basePath = "http://" + host + "/v1" - - // Kafka - kafkaHost = "kafka:9092" - consumerTopic = "consumer_topic" - producerTopic = "producer_topic" ) func TestMain(t *testing.M) { @@ -41,12 +36,12 @@ func healthCheck(attempts int) error { var err error for attempts > 0 { - err = Do(Get(healthPath), Expect().Status().Equal(http.StatusOK)) + err = Do(Get(basePath+healthPath), Expect().Status().Equal(http.StatusOK)) if err == nil { return nil } - log.Printf("Integration tests: url %s is not available, attempts left: %d", healthPath, attempts) + log.Printf("Integration tests: url %s is not available, attempts left: %d", basePath+healthPath, attempts) time.Sleep(time.Second) diff --git a/backend/internal/controller/http/v1/utils.go b/backend/internal/controller/http/v1/utils.go index 80ae2b17..a6da936f 100644 --- a/backend/internal/controller/http/v1/utils.go +++ b/backend/internal/controller/http/v1/utils.go @@ -36,7 +36,10 @@ func (m *HTTPControllerMessenger) SendMessage(chanName string, value interface{} } res := <-channel - notify.Stop(msgKey, channel) + err = notify.Stop(msgKey, channel) + if err != nil { + return &entity.NotifyData{}, fmt.Errorf("sendMessage - notify.Stop: %v", err) + } if notifyData, ok := res.(*entity.NotifyData); ok { switch msg := notifyData.Message.(type) { case entity.EnvelopeResponse: diff --git a/backend/internal/usecase/role_permission_test.go b/backend/internal/usecase/role_permission_test.go index a8dca681..65040277 100644 --- a/backend/internal/usecase/role_permission_test.go +++ b/backend/internal/usecase/role_permission_test.go @@ -11,16 +11,6 @@ import ( "github.com/stretchr/testify/assert" ) -var validateError = errors.New("error") - -type rolePermissionTest struct { - name string - roleId int - mock func() - res interface{} - err error -} - func TestRolePermissionUseCase_Validate(t *testing.T) { t.Helper() diff --git a/backend/internal/usecase/users.go b/backend/internal/usecase/users.go index 25c6a52c..20fe69bc 100644 --- a/backend/internal/usecase/users.go +++ b/backend/internal/usecase/users.go @@ -41,7 +41,11 @@ func (uc *UserUseCase) CreateUser(user entity.User) error { if err != nil { return err } - uc.repo.CreateUser(user) + + err = uc.repo.CreateUser(user) + if err != nil { + return err + } return nil } @@ -91,11 +95,10 @@ func (uc *UserUseCase) GetAllUsers() ([]entity.UserResponse, error) { } func (uc *UserUseCase) EditUsersRole(userRole entity.UserRole) error { - var err error + err := uc.repo.EditUsersRole(userRole.ID_user, userRole.ID_role) if err != nil { return err } - uc.repo.EditUsersRole(userRole.ID_user, userRole.ID_role) return nil } diff --git a/backend/internal/usecase/vault_test.go b/backend/internal/usecase/vault_test.go index 90d2861d..6d6644aa 100644 --- a/backend/internal/usecase/vault_test.go +++ b/backend/internal/usecase/vault_test.go @@ -246,6 +246,3 @@ func TestVaultUseCaseCreate(t *testing.T) { }) } } - -func TestsVaultUseCaseUpdateVault(t *testing.T) { -} diff --git a/backend/pkg/kafka/connection.go b/backend/pkg/kafka/connection.go index 4702b475..39641001 100644 --- a/backend/pkg/kafka/connection.go +++ b/backend/pkg/kafka/connection.go @@ -101,7 +101,11 @@ func (c Connection) Run(cfg *config.Config, chanName string) { fmt.Println(err) continue } - notify.Post(string(msg.Key), &entity.NotifyData{Key: string(msg.Key), Message: data}) + err = notify.Post(string(msg.Key), &entity.NotifyData{Key: string(msg.Key), Message: data}) + if err != nil { + fmt.Println(err) + continue + } } } } From be24f1fd8ef22a9fc46eda9c5d7987c5c86c8ad9 Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Tue, 10 Oct 2023 14:55:20 -0300 Subject: [PATCH 15/16] feat: update migrate toml --- backend/migrations/{000027_toml.down.sql => 000028_toml.down.sql} | 0 backend/migrations/{000027_toml.up.sql => 000028_toml.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename backend/migrations/{000027_toml.down.sql => 000028_toml.down.sql} (100%) rename backend/migrations/{000027_toml.up.sql => 000028_toml.up.sql} (100%) diff --git a/backend/migrations/000027_toml.down.sql b/backend/migrations/000028_toml.down.sql similarity index 100% rename from backend/migrations/000027_toml.down.sql rename to backend/migrations/000028_toml.down.sql diff --git a/backend/migrations/000027_toml.up.sql b/backend/migrations/000028_toml.up.sql similarity index 100% rename from backend/migrations/000027_toml.up.sql rename to backend/migrations/000028_toml.up.sql From 1f224b94290d980f537bb6367ed03fd977915b00 Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Fri, 20 Oct 2023 10:42:35 -0300 Subject: [PATCH 16/16] feat: get toml data --- backend/internal/controller/http/v1/assets.go | 22 +++- backend/internal/controller/http/v1/router.go | 5 +- backend/internal/entity/asset.go | 114 +++++++++--------- backend/internal/usecase/assets.go | 14 +++ .../internal/usecase/repo/toml_postgres.go | 6 +- backend/pkg/toml/toml_helpers.go | 5 +- 6 files changed, 98 insertions(+), 68 deletions(-) diff --git a/backend/internal/controller/http/v1/assets.go b/backend/internal/controller/http/v1/assets.go index fbf69eb3..652762be 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -50,6 +50,7 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/:id/image", r.uploadAssetImage) h.POST("/generate-toml", r.generateTOML) h.PUT("/update-toml", r.updateTOML) + h.GET("/toml-data", r.getTomlData) } } @@ -879,7 +880,7 @@ func (r *assetsRoutes) generateTOML(c *gin.Context) { errorResponse(c, http.StatusBadRequest, "invalid request body: %s", err) return } - + fmt.Printf(request.Currencies[len(request.Currencies)-1].Description) toml, err := r.as.CreateToml(request) if err != nil { errorResponse(c, http.StatusInternalServerError, "error creating TOML", err) @@ -933,3 +934,22 @@ func (r *assetsRoutes) updateTOML(c *gin.Context) { c.Data(http.StatusOK, "application/toml", []byte(toml)) } + +// @Summary Get TOML data +// @Description Get TOML data +// @Tags Assets +// @Accept json +// @Produce json +// @Param asset_issuer path string true "Asset issuer" +// @Success 200 {object} entity.TomlData +// @Failure 500 {object} response +// @Router /assets/toml [get] +func (r *assetsRoutes) getTomlData(c *gin.Context) { + tomlContent, err := r.as.GetTomlData() + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error retrieving TOML", err) + return + } + + c.JSON(http.StatusOK, tomlContent) +} diff --git a/backend/internal/controller/http/v1/router.go b/backend/internal/controller/http/v1/router.go index 4c90eee0..87f51695 100644 --- a/backend/internal/controller/http/v1/router.go +++ b/backend/internal/controller/http/v1/router.go @@ -86,9 +86,6 @@ func NewRouter( newVaultRoutes(groupV1, messengerController, authUseCase, vaultUc, vaultCategoryUc, walletUseCase, assetUseCase) newContractRoutes(groupV1, messengerController, authUseCase, contractUc, vaultUc, assetUseCase) newLogTransactionsRoutes(groupV1, walletUseCase, assetUseCase, messengerController, logUc, authUseCase) + newAssetTomlRoutes(groupV1, walletUseCase, assetUseCase, messengerController, authUseCase, logUc) } - - // Toml Route - handler.Use(CORSMiddlewareAllowAllOrigins()) // Allow all origins for the toml - newAssetTomlRoutes(handler.Group("/"), walletUseCase, assetUseCase, messengerController, authUseCase, logUc) } diff --git a/backend/internal/entity/asset.go b/backend/internal/entity/asset.go index 2362bf84..66eb9568 100644 --- a/backend/internal/entity/asset.go +++ b/backend/internal/entity/asset.go @@ -20,75 +20,75 @@ const ( ) type TomlData struct { - Version string `toml:"VERSION,omitempty"` - NetworkPassphrase string `toml:"NETWORK_PASSPHRASE,omitempty"` - FederationServer string `toml:"FEDERATION_SERVER,omitempty"` - TransferServer string `toml:"TRANSFER_SERVER,omitempty"` - SigningKey string `toml:"SIGNING_KEY,omitempty"` - HorizonURL string `toml:"HORIZON_URL,omitempty"` - Accounts []string `toml:"ACCOUNTS,omitempty"` - Documentation Documentation `toml:"DOCUMENTATION,omitempty"` - Principals []Principal `toml:"PRINCIPALS,omitempty"` - Currencies []Currency `toml:"CURRENCIES,omitempty"` - Validators []Validator `toml:"VALIDATORS,omitempty"` + Version string `json:"VERSION,omitempty"` + NetworkPassphrase string `json:"NETWORK_PASSPHRASE,omitempty"` + FederationServer string `json:"FEDERATION_SERVER,omitempty"` + TransferServer string `json:"TRANSFER_SERVER,omitempty"` + SigningKey string `json:"SIGNING_KEY,omitempty"` + HorizonURL string `json:"HORIZON_URL,omitempty"` + Accounts []string `json:"ACCOUNTS,omitempty"` + Documentation Documentation `json:"DOCUMENTATION,omitempty"` + Principals []Principal `json:"PRINCIPALS,omitempty"` + Currencies []Currency `json:"CURRENCIES,omitempty"` + Validators []Validator `json:"VALIDATORS,omitempty"` } type Documentation struct { - OrgName string `toml:"ORG_NAME,omitempty"` - OrgDBA string `toml:"ORG_DBA,omitempty"` - OrgURL string `toml:"ORG_URL,omitempty"` - OrgLogo string `toml:"ORG_LOGO,omitempty"` - OrgDescription string `toml:"ORG_DESCRIPTION,omitempty"` - OrgPhysicalAddress string `toml:"ORG_PHYSICAL_ADDRESS,omitempty"` - OrgPhysicalAddressAttestation string `toml:"ORG_PHYSICAL_ADDRESS_ATTESTATION,omitempty"` - OrgPhoneNumber string `toml:"ORG_PHONE_NUMBER,omitempty"` - OrgPhoneNumberAttestation string `toml:"ORG_PHONE_NUMBER_ATTESTATION,omitempty"` - OrgKeybase string `toml:"ORG_KEYBASE,omitempty"` - OrgTwitter string `toml:"ORG_TWITTER,omitempty"` - OrgGithub string `toml:"ORG_GITHUB,omitempty"` - OrgOfficialEmail string `toml:"ORG_OFFICIAL_EMAIL,omitempty"` + OrgName string `json:"ORG_NAME,omitempty"` + OrgDBA string `json:"ORG_DBA,omitempty"` + OrgURL string `json:"ORG_URL,omitempty"` + OrgLogo string `json:"ORG_LOGO,omitempty"` + OrgDescription string `json:"ORG_DESCRIPTION,omitempty"` + OrgPhysicalAddress string `json:"ORG_PHYSICAL_ADDRESS,omitempty"` + OrgPhysicalAddressAttestation string `json:"ORG_PHYSICAL_ADDRESS_ATTESTATION,omitempty"` + OrgPhoneNumber string `json:"ORG_PHONE_NUMBER,omitempty"` + OrgPhoneNumberAttestation string `json:"ORG_PHONE_NUMBER_ATTESTATION,omitempty"` + OrgKeybase string `json:"ORG_KEYBASE,omitempty"` + OrgTwitter string `json:"ORG_TWITTER,omitempty"` + OrgGithub string `json:"ORG_GITHUB,omitempty"` + OrgOfficialEmail string `json:"ORG_OFFICIAL_EMAIL,omitempty"` } type Principal struct { - Name string `toml:"name,omitempty"` - Email string `toml:"email,omitempty"` - Keybase string `toml:"keybase,omitempty"` - Twitter string `toml:"twitter,omitempty"` - Github string `toml:"github,omitempty"` - IDPhotoHash string `toml:"id_photo_hash,omitempty"` - VerificationPhotoHash string `toml:"verification_photo_hash,omitempty"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Keybase string `json:"keybase,omitempty"` + Twitter string `json:"twitter,omitempty"` + Github string `json:"github,omitempty"` + IDPhotoHash string `json:"id_photo_hash,omitempty"` + VerificationPhotoHash string `json:"verification_photo_hash,omitempty"` } type Currency struct { - Code string `toml:"code,omitempty"` - Issuer string `toml:"issuer,omitempty"` - DisplayDecimals int `toml:"display_decimals,omitempty"` - Name string `toml:"name,omitempty"` - Description string `toml:"desc,omitempty"` - Conditions string `toml:"conditions,omitempty"` - Image string `toml:"image,omitempty"` - FixedNumber int `toml:"fixed_number,omitempty"` - MaxNumber int `toml:"max_number,omitempty"` - IsUnlimited bool `toml:"is_unlimited,omitempty"` - IsAssetAnchored bool `toml:"is_asset_anchored,omitempty"` - AnchorAssetType string `toml:"anchor_asset_type,omitempty"` - AnchorAsset string `toml:"anchor_asset,omitempty"` - AttestationOfReserve string `toml:"attestation_of_reserve,omitempty"` - RedemptionInstructions string `toml:"redemption_instructions,omitempty"` - CollateralAddresses []string `toml:"collateral_addresses,omitempty"` - CollateralAddressMessages []string `toml:"collateral_address_messages,omitempty"` - CollateralAddressSignatures []string `toml:"collateral_address_signatures,omitempty"` - Regulated bool `toml:"regulated,omitempty"` - ApprovalServer string `toml:"approval_server,omitempty"` - ApprovalCriteria string `toml:"approval_criteria,omitempty"` + Code string `json:"code,omitempty"` + Issuer string `json:"issuer,omitempty"` + DisplayDecimals int `json:"display_decimals,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"desc,omitempty"` + Conditions string `json:"conditions,omitempty"` + Image string `json:"image,omitempty"` + FixedNumber int `json:"fixed_number,omitempty"` + MaxNumber int `json:"max_number,omitempty"` + IsUnlimited bool `json:"is_unlimited,omitempty"` + IsAssetAnchored bool `json:"is_asset_anchored,omitempty"` + AnchorAssetType string `json:"anchor_asset_type,omitempty"` + AnchorAsset string `json:"anchor_asset,omitempty"` + AttestationOfReserve string `json:"attestation_of_reserve,omitempty"` + RedemptionInstructions string `json:"redemption_instructions,omitempty"` + CollateralAddresses []string `json:"collateral_addresses,omitempty"` + CollateralAddressMessages []string `json:"collateral_address_messages,omitempty"` + CollateralAddressSignatures []string `json:"collateral_address_signatures,omitempty"` + Regulated bool `json:"regulated,omitempty"` + ApprovalServer string `json:"approval_server,omitempty"` + ApprovalCriteria string `json:"approval_criteria,omitempty"` } type Validator struct { - Alias string `toml:"ALIAS"` - DisplayName string `toml:"DISPLAY_NAME"` - Host string `toml:"HOST"` - PublicKey string `toml:"PUBLIC_KEY"` - History string `toml:"HISTORY"` + Alias string `json:"ALIAS"` + DisplayName string `json:"DISPLAY_NAME"` + Host string `json:"HOST"` + PublicKey string `json:"PUBLIC_KEY"` + History string `json:"HISTORY"` } type Toml struct { diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index 62f7b1f0..6a19d8fa 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -145,3 +145,17 @@ func (uc *AssetUseCase) UpdateToml(updatedToml entity.TomlData) (string, error) return tomlCreated, err } + +func (uc *AssetUseCase) GetTomlData() (entity.TomlData, error) { + tomlDb, err := uc.tRepo.GetToml() + if err != nil { + return entity.TomlData{}, fmt.Errorf("AssetUseCase - GetTomlData - uc.repo.GetToml: %w", err) + } + + tomParsed, err := uc.tInt.RetrieveToml(tomlDb) // Parse old toml data + if err != nil { + return entity.TomlData{}, fmt.Errorf("AssetUseCase - GetTomlData - uc.tInt.RetrieveToml: %w", err) + } + + return tomParsed, err +} diff --git a/backend/internal/usecase/repo/toml_postgres.go b/backend/internal/usecase/repo/toml_postgres.go index 1ed3dc07..502d572d 100644 --- a/backend/internal/usecase/repo/toml_postgres.go +++ b/backend/internal/usecase/repo/toml_postgres.go @@ -29,9 +29,7 @@ func (r TomlRepoInterface) CreateToml(content string) (string, error) { func (r TomlRepoInterface) GetToml() (string, error) { var res string stmt := `SELECT content FROM toml ORDER BY id DESC LIMIT 1;` - err := r.Db.QueryRow(stmt).Scan(&res) - if err != nil { - return "", fmt.Errorf("AssetRepo - GetToml - db.QueryRow: %w", err) - } + r.Db.QueryRow(stmt).Scan(&res) + return res, nil } diff --git a/backend/pkg/toml/toml_helpers.go b/backend/pkg/toml/toml_helpers.go index c9cd687b..767905c1 100644 --- a/backend/pkg/toml/toml_helpers.go +++ b/backend/pkg/toml/toml_helpers.go @@ -20,9 +20,10 @@ func updateFieldsIfEmpty(fields []FieldConfig) { func appendIfNotExists[T any](existing []T, newItems []T, equals func(T, T) bool) []T { for _, newItem := range newItems { found := false - for _, item := range existing { + for index, item := range existing { if equals(item, newItem) { found = true + existing[index] = newItem break } } @@ -41,7 +42,7 @@ func appendIfNotExistsPrincipal(existing []entity.Principal, newItems []entity.P func appendIfNotExistsCurrency(existing []entity.Currency, newItems []entity.Currency) []entity.Currency { return appendIfNotExists(existing, newItems, func(item1, item2 entity.Currency) bool { - return item1.Code == item2.Code + return item1.Code == item2.Code && item1.Issuer == item2.Issuer }) }