diff --git a/backend/.env.example b/backend/.env.example index 5b4429f0..14cbb80e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -22,6 +22,7 @@ KAFKA_SCHEMA_REGISTRY_URL= # HTTP HTTP_PORT= HTTP_FRONT_ADRESS= +HTTP_FRONT_ADRESS= ## JWT SECRET JWT_SECRET_KEY=secret @@ -61,3 +62,4 @@ PRINCIPALS_TWITTER= PRINCIPALS_GITHUB= PRINCIPALS_ID_PHOTO_HASH= PRINCIPALS_VERIFICATION_PHOTO_HASH= + diff --git a/backend/docs/docs.go b/backend/docs/docs.go index b2d449df..1b42ee8f 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -45,6 +45,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" + } + } + } + } + }, "/asset": { "get": { "description": "Get asset by id", @@ -312,6 +350,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/entity.TomlData" + } + }, + "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", @@ -416,6 +500,52 @@ const docTemplate = `{ } } }, + "/assets/update-toml": { + "put": { + "description": "Update a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Update 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/entity.TomlData" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/{id}/image": { "post": { "description": "Upload a base64 encoded image for a specific asset by ID", @@ -1535,7 +1665,7 @@ const docTemplate = `{ "summary": "Create a new vault", "parameters": [ { - "description": "Vault info", + "description": "CreateVaultRequest", "name": "request", "in": "body", "required": true, @@ -2092,6 +2222,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": { @@ -2178,6 +2429,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": { @@ -2265,6 +2542,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": { @@ -2341,6 +2668,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 0f34429b..9c471112 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -33,6 +33,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" + } + } + } + } + }, "/asset": { "get": { "description": "Get asset by id", @@ -300,6 +338,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/entity.TomlData" + } + }, + "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", @@ -404,6 +488,52 @@ } } }, + "/assets/update-toml": { + "put": { + "description": "Update a TOML file", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Update 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/entity.TomlData" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/v1.response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } + }, "/assets/{id}/image": { "post": { "description": "Upload a base64 encoded image for a specific asset by ID", @@ -1523,7 +1653,7 @@ "summary": "Create a new vault", "parameters": [ { - "description": "Vault info", + "description": "CreateVaultRequest", "name": "request", "in": "body", "required": true, @@ -2080,6 +2210,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": { @@ -2166,6 +2417,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": { @@ -2253,6 +2530,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": { @@ -2329,6 +2656,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 0cbb94a5..eb07bffd 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -49,6 +49,86 @@ definitions: yield_rate: type: integer 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: @@ -111,6 +191,23 @@ definitions: example: name type: string 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: admin: @@ -170,6 +267,39 @@ definitions: type: integer type: array 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: @@ -220,6 +350,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: active: @@ -603,6 +746,31 @@ paths: summary: Get contract tags: - Contract + /.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 /asset: get: consumes: @@ -836,6 +1004,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/entity.TomlData' + "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: @@ -904,6 +1102,36 @@ paths: summary: Transfer an asset tags: - Assets + /assets/update-toml: + put: + consumes: + - application/json + description: Update 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/entity.TomlData' + "400": + description: Bad Request + schema: + $ref: '#/definitions/v1.response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/v1.response' + summary: Update a TOML file + tags: + - Assets /contract: post: consumes: @@ -1575,7 +1803,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 b5ca8952..0ed37181 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 @@ -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= 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/app/app.go b/backend/internal/app/app.go index dbac1407..00c7debb 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -17,10 +17,11 @@ 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" ) // Run creates objects via constructors. -func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface) { +func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface, tRepo *toml.DefaultTomlGenerator) { // Use cases authUc := usecase.NewAuthUseCase( repo.New(pg), cfg.JWT.SecretKey, @@ -34,6 +35,9 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv, pSub, pSig assetUc := usecase.NewAssetUseCase( repo.NewAssetRepo(pg), repo.NewWalletRepo(pg), + tRepo, + repo.NewTomlRepo(pg), + 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 fa5855f2..652762be 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -24,6 +24,14 @@ type assetsRoutes struct { l usecase.LogTransactionUseCase } +func newAssetTomlRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as usecase.AssetUseCase, m HTTPControllerMessenger, a usecase.AuthUseCase, l usecase.LogTransactionUseCase) { + r := &assetsRoutes{w, as, m, a, l} + 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, l usecase.LogTransactionUseCase) { r := &assetsRoutes{w, as, m, a, l} @@ -40,6 +48,9 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/transfer", r.transferAsset) h.GET("/:id", r.getAssetById) h.POST("/:id/image", r.uploadAssetImage) + h.POST("/generate-toml", r.generateTOML) + h.PUT("/update-toml", r.updateTOML) + h.GET("/toml-data", r.getTomlData) } } @@ -274,6 +285,30 @@ func (r *assetsRoutes) createAsset(c *gin.Context) { return } + // 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(tomlData) + if err != nil { + errorResponse(c, http.StatusNotFound, "error to create TOML ", err) + return + } + } + + _, err = r.as.UpdateToml(tomlData) + if err != nil { + errorResponse(c, http.StatusNotFound, "error to update TOML ", err) + return + } + c.JSON(http.StatusOK, asset) } @@ -828,3 +863,93 @@ func (r *assetsRoutes) getAssetImage(c *gin.Context) { c.Data(http.StatusOK, "image/png", image) } + +// @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} entity.TomlData +// @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, "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) + 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} entity.TomlData +// @Failure 500 {object} response +// @Router /.well-known/stellar.toml [get] +func (r *assetsRoutes) retrieveToml(c *gin.Context) { + tomlContent, err := r.as.RetrieveToml() + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error retrieving TOML", err) + return + } + + 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, "invalid request body", err) + return + } + + toml, err := r.as.UpdateToml(request) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error updating TOML", err) + return + } + + 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 f1531d0c..f378b479 100644 --- a/backend/internal/controller/http/v1/router.go +++ b/backend/internal/controller/http/v1/router.go @@ -4,15 +4,29 @@ 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) @@ -32,7 +46,7 @@ func CORSMiddleware(cfg config.HTTP) gin.HandlerFunc { // Swagger spec: // @title Token Factory API // @version 1.0 -// @BasePath /v1 +// @BasePath / func NewRouter( handler *gin.Engine, pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface, @@ -60,6 +74,7 @@ func NewRouter( handler.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) // Routers handler.Use(CORSMiddleware(cfg)) // Alow only frontend origin + handler.Use(CORSMiddleware(cfg)) // Alow only frontend origin groupV1 := handler.Group("/v1") { newUserRoutes(groupV1, userUseCase, authUseCase, rolePermissionUc) @@ -72,5 +87,6 @@ func NewRouter( newContractRoutes(groupV1, messengerController, authUseCase, contractUc, vaultUc, assetUseCase) newLogTransactionsRoutes(groupV1, walletUseCase, assetUseCase, messengerController, logUc, authUseCase) newSorobanRoutes(groupV1, walletUseCase, messengerController, authUseCase) + newAssetTomlRoutes(groupV1, walletUseCase, assetUseCase, messengerController, authUseCase, logUc) } } diff --git a/backend/internal/controller/http/v1/utils.go b/backend/internal/controller/http/v1/utils.go index d734dbf9..10ca03f9 100644 --- a/backend/internal/controller/http/v1/utils.go +++ b/backend/internal/controller/http/v1/utils.go @@ -37,7 +37,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/controller/http/v1/vault.go b/backend/internal/controller/http/v1/vault.go index 4e1b9b4e..9129f43a 100644 --- a/backend/internal/controller/http/v1/vault.go +++ b/backend/internal/controller/http/v1/vault.go @@ -57,7 +57,7 @@ type UpdateVaultAssetRequest 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 892c6ccc..66eb9568 100644 --- a/backend/internal/entity/asset.go +++ b/backend/internal/entity/asset.go @@ -18,3 +18,82 @@ const ( PaymentToken = "payment_token" DefiToken = "defi_token" ) + +type TomlData struct { + 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 `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 `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 `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 `json:"ALIAS"` + DisplayName string `json:"DISPLAY_NAME"` + Host string `json:"HOST"` + PublicKey string `json:"PUBLIC_KEY"` + History string `json:"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 6099d315..6a19d8fa 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -3,18 +3,25 @@ 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 + tInt TomlInterface + tRepo TomlRepoInterface + cfg config.Horizon } -func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface) *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, } } @@ -81,3 +88,74 @@ func (uc *AssetUseCase) GetImage(assetId string) ([]byte, error) { } return image, nil } + +func (uc *AssetUseCase) CreateToml(req entity.TomlData) (string, error) { + tomlCreated, err := uc.tInt.GenerateToml(req, 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) + } + + return tomlCreated, err +} + +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 in the database + oldTomlDb, err := uc.tRepo.GetToml() + if err != nil { + return "", fmt.Errorf("AssetUseCase - RetrieveToml - uc.repo.GetToml: %w", err) + } + + 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 + newTomlParsed, err := uc.tInt.UpdateTomlData(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) + 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) + } + + 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/assets_test.go b/backend/internal/usecase/assets_test.go index 8cebd790..426c2ed3 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,15 @@ 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) + tr := mocks.NewMockTomlRepoInterface(mockCtl) + u := usecase.NewAssetUseCase(ra, rw, tg, tr, 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 +203,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/interfaces.go b/backend/internal/usecase/interfaces.go index c276e04f..a3cf1c86 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -3,6 +3,7 @@ package usecase import ( "time" + "github.com/CheesecakeLabs/token-factory-v2/backend/config" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" ) @@ -69,6 +70,17 @@ type ( CreateRolePermission(entity.RolePermissionRequest) (entity.RolePermissionRequest, error) } + TomlInterface interface { + GenerateToml(entity.TomlData, config.Horizon) (string, error) + RetrieveToml(string) (entity.TomlData, error) + UpdateTomlData(entity.TomlData, entity.TomlData) (entity.TomlData, error) + } + + TomlRepoInterface interface { + CreateToml(string) (string, error) + GetToml() (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 841773e7..c70a1c6c 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -8,6 +8,7 @@ import ( reflect "reflect" time "time" + 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" @@ -685,6 +686,127 @@ 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) (entity.TomlData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RetrieveToml", arg0) + ret0, _ := ret[0].(entity.TomlData) + 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) +} + +// 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 + 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/asset_postgres.go b/backend/internal/usecase/repo/asset_postgres.go index 67d20f38..ef96f46a 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/internal/usecase/repo/toml_postgres.go b/backend/internal/usecase/repo/toml_postgres.go new file mode 100644 index 00000000..502d572d --- /dev/null +++ b/backend/internal/usecase/repo/toml_postgres.go @@ -0,0 +1,35 @@ +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;` + r.Db.QueryRow(stmt).Scan(&res) + + return res, nil +} 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/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/main.go b/backend/main.go index 1449c99e..365f22a4 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) @@ -71,5 +74,5 @@ func main() { } go signConn.Run(cfg, entity.SignChannel) - app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer, submitConn.Producer, signConn.Producer) + app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer, submitConn.Producer, signConn.Producer, tRepo) } 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/000028_toml.down.sql b/backend/migrations/000028_toml.down.sql new file mode 100644 index 00000000..37c630d2 --- /dev/null +++ b/backend/migrations/000028_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/000028_toml.up.sql b/backend/migrations/000028_toml.up.sql new file mode 100644 index 00000000..0dd1258e --- /dev/null +++ b/backend/migrations/000028_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 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 + } } } } diff --git a/backend/pkg/toml/toml.go b/backend/pkg/toml/toml.go new file mode 100644 index 00000000..d48ab600 --- /dev/null +++ b/backend/pkg/toml/toml.go @@ -0,0 +1,93 @@ +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) { + var principalFieldConfigs []FieldConfig + + fieldConfigs := []FieldConfig{ + {&req.Version, cfg.StellarTomlVersion}, + {&req.NetworkPassphrase, cfg.TestNetworkPass}, + {&req.FederationServer, cfg.FederationServer}, + {&req.TransferServer, cfg.TransferServer}, + {&req.HorizonURL, cfg.HorizonURL}, + } + + 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}, + } + + if len(req.Principals) > 0 { + 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 + } + 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) + if err != nil { + return entity.TomlData{}, err + } + return cfg, nil +} diff --git a/backend/pkg/toml/toml_helpers.go b/backend/pkg/toml/toml_helpers.go new file mode 100644 index 00000000..767905c1 --- /dev/null +++ b/backend/pkg/toml/toml_helpers.go @@ -0,0 +1,53 @@ +package toml + +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" +) + +type FieldConfig struct { + Field *string + ConfigVal string +} + +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 { + for _, newItem := range newItems { + found := false + for index, item := range existing { + if equals(item, newItem) { + found = true + existing[index] = newItem + 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 && item1.Issuer == item2.Issuer + }) +} + +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/stellar-kms b/stellar-kms index b443a344..630eba7d 160000 --- a/stellar-kms +++ b/stellar-kms @@ -1 +1 @@ -Subproject commit b443a3444c265291d24c6a34a7b5add928c1e562 +Subproject commit 630eba7d3e7ea4083a0c0824153ed64e20f84828