diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9382b808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*.png diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/backend/.env.example b/backend/.env.example index 4b281967..183b569b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,6 +5,9 @@ PG_USER= PG_PASSWORD= PG_DB_NAME= +# Deploy Stage +DEPLOY_STAGE=local + # Kafka KAFKA_CLIENT_GROUP_ID= KAFKA_CLIENT_BROKERS= @@ -14,9 +17,13 @@ KAFKA_ENVELOPE_PRODUCER_TOPIC= KAFKA_ENVELOPE_CONSUMER_TOPICS= KAFKA_HORIZON_PRODUCER_TOPIC= KAFKA_HORIZON_CONSUMER_TOPICS= +KAFKA_SUBMIT_TRANSACTION_PRODUCER_TOPIC= +KAFKA_SIGN_SOROBAN_TRANSACTION_CONSUMER_TOPICS= +KAFKA_SIGN_SOROBAN_TRANSACTION_PRODUCER_TOPIC= + KAFKA_SCHEMA_REGISTRY_URL= -# Log +# Log LOG_LEVEL=debug # HTTP @@ -29,6 +36,9 @@ JWT_SECRET_KEY=secret ## Production environment GIN_MODE=developer #release +AWS_BUCKET_NAME= +AWS_REGION= + ## Horizon Config HORIZON_PUBLIC_API_SERVER=https://horizon-testnet.stellar.org HORIZON_TEST_API_SERVER=https://horizon-testnet.stellar.org diff --git a/backend/.gitignore b/backend/.gitignore index 655f0217..a0783c3f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -25,4 +25,4 @@ starlabs # vendor/ # Go workspace file -go.work \ No newline at end of file +go.work diff --git a/backend/config/config.go b/backend/config/config.go index d838fccd..5c8b8bad 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -13,6 +13,8 @@ type ( JWT JWT Horizon Horizon Log Log + AWS AWS + Deploy Deploy } Log struct { @@ -35,6 +37,14 @@ type ( ConsumerTopics []string `env:"KAFKA_HORIZON_CONSUMER_TOPICS"` ProducerTopic string `env:"KAFKA_HORIZON_PRODUCER_TOPIC"` } + SubmitTransactionCfg struct { + ConsumerTopics []string `env:"KAFKA_ENVELOPE_CONSUMER_TOPICS"` + ProducerTopic string `env:"KAFKA_SUBMIT_TRANSACTION_PRODUCER_TOPIC"` + } + SignTransactionCfg struct { + ConsumerTopics []string `env:"KAFKA_SIGN_SOROBAN_TRANSACTION_CONSUMER_TOPICS"` + ProducerTopic string `env:"KAFKA_SIGN_SOROBAN_TRANSACTION_PRODUCER_TOPIC"` + } } PGConfig struct { @@ -54,6 +64,11 @@ type ( SecretKey string `env-required:"true" env:"JWT_SECRET_KEY"` } + AWS struct { + BucketName string `env-required:"true" env:"AWS_BUCKET_NAME"` + AwsRegion string `env:"AWS_REGION"` + } + Horizon struct { PublicAPIServer string `env:"HORIZON_PUBLIC_API_SERVER"` TestAPIServer string `env:"HORIZON_TEST_API_SERVER"` @@ -92,6 +107,10 @@ type ( IDPhotoHash string `env:"PRINCIPALS_ID_PHOTO_HASH"` VerificationPhotoHash string `env:"PRINCIPALS_VERIFICATION_PHOTO_HASH"` } + + Deploy struct { + DeployStage string `env:"DEPLOY_STAGE"` + } ) // NewConfig returns app config. diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 52dca575..4dd607b3 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -142,10 +142,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.Asset" - } + "$ref": "#/definitions/v1.PaginatedAssetsResponse" } }, "500": { @@ -1492,6 +1489,98 @@ const docTemplate = `{ } } }, + "/soroban-transactions/sign": { + "post": { + "description": "Signed a XDR transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Soroban" + ], + "summary": "Signed Transaction", + "parameters": [ + { + "description": "Signed a XDR transaction", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SignedTransactionRequest" + } + } + ], + "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" + } + } + } + } + }, + "/soroban-transactions/submit": { + "post": { + "description": "Submit a XDR transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Soroban" + ], + "summary": "Submit Transaction", + "parameters": [ + { + "description": "Submit a XDR transaction", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SubmitTransactionRequest" + } + } + ], + "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" + } + } + } + } + }, "/user/create": { "post": { "description": "Create user", @@ -2177,6 +2266,35 @@ const docTemplate = `{ } } } + }, + "/wallets/sponsor_pk/": { + "get": { + "description": "Get Sponsor Public Key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wallets" + ], + "summary": "Get Sponsor Public Key", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } } }, "definitions": { @@ -3034,6 +3152,20 @@ const docTemplate = `{ } } }, + "v1.PaginatedAssetsResponse": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Asset" + } + }, + "totalPages": { + "type": "integer" + } + } + }, "v1.RolePermissionRequest": { "type": "object", "properties": { @@ -3051,6 +3183,34 @@ const docTemplate = `{ } } }, + "v1.SignedTransactionRequest": { + "type": "object", + "required": [ + "envelope" + ], + "properties": { + "envelope": { + "type": "string", + "example": "KJDSKD..." + }, + "wallet_pk": { + "type": "string", + "example": "GDSKJG..." + } + } + }, + "v1.SubmitTransactionRequest": { + "type": "object", + "required": [ + "envelope" + ], + "properties": { + "envelope": { + "type": "string", + "example": "KJDSKD..." + } + } + }, "v1.TransferAssetRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index e45fb66d..1efebc25 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -131,10 +131,7 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.Asset" - } + "$ref": "#/definitions/v1.PaginatedAssetsResponse" } }, "500": { @@ -1481,6 +1478,98 @@ } } }, + "/soroban-transactions/sign": { + "post": { + "description": "Signed a XDR transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Soroban" + ], + "summary": "Signed Transaction", + "parameters": [ + { + "description": "Signed a XDR transaction", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SignedTransactionRequest" + } + } + ], + "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" + } + } + } + } + }, + "/soroban-transactions/submit": { + "post": { + "description": "Submit a XDR transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Soroban" + ], + "summary": "Submit Transaction", + "parameters": [ + { + "description": "Submit a XDR transaction", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SubmitTransactionRequest" + } + } + ], + "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" + } + } + } + } + }, "/user/create": { "post": { "description": "Create user", @@ -2166,6 +2255,35 @@ } } } + }, + "/wallets/sponsor_pk/": { + "get": { + "description": "Get Sponsor Public Key", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wallets" + ], + "summary": "Get Sponsor Public Key", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/v1.response" + } + } + } + } } }, "definitions": { @@ -3023,6 +3141,20 @@ } } }, + "v1.PaginatedAssetsResponse": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Asset" + } + }, + "totalPages": { + "type": "integer" + } + } + }, "v1.RolePermissionRequest": { "type": "object", "properties": { @@ -3040,6 +3172,34 @@ } } }, + "v1.SignedTransactionRequest": { + "type": "object", + "required": [ + "envelope" + ], + "properties": { + "envelope": { + "type": "string", + "example": "KJDSKD..." + }, + "wallet_pk": { + "type": "string", + "example": "GDSKJG..." + } + } + }, + "v1.SubmitTransactionRequest": { + "type": "object", + "required": [ + "envelope" + ], + "properties": { + "envelope": { + "type": "string", + "example": "KJDSKD..." + } + } + }, "v1.TransferAssetRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 2096935f..663e3455 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -590,6 +590,15 @@ definitions: - code - id type: object + v1.PaginatedAssetsResponse: + properties: + assets: + items: + $ref: '#/definitions/entity.Asset' + type: array + totalPages: + type: integer + type: object v1.RolePermissionRequest: properties: is_add: @@ -602,6 +611,25 @@ definitions: example: 1 type: integer type: object + v1.SignedTransactionRequest: + properties: + envelope: + example: KJDSKD... + type: string + wallet_pk: + example: GDSKJG... + type: string + required: + - envelope + type: object + v1.SubmitTransactionRequest: + properties: + envelope: + example: KJDSKD... + type: string + required: + - envelope + type: object v1.TransferAssetRequest: properties: amount: @@ -790,9 +818,7 @@ paths: "200": description: OK schema: - items: - $ref: '#/definitions/entity.Asset' - type: array + $ref: '#/definitions/v1.PaginatedAssetsResponse' "500": description: Internal Server Error schema: @@ -1673,6 +1699,66 @@ paths: summary: Update a role tags: - Role + /soroban-transactions/sign: + post: + consumes: + - application/json + description: Signed a XDR transaction + parameters: + - description: Signed a XDR transaction + in: body + name: request + required: true + schema: + $ref: '#/definitions/v1.SignedTransactionRequest' + 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: Signed Transaction + tags: + - Soroban + /soroban-transactions/submit: + post: + consumes: + - application/json + description: Submit a XDR transaction + parameters: + - description: Submit a XDR transaction + in: body + name: request + required: true + schema: + $ref: '#/definitions/v1.SubmitTransactionRequest' + 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: Submit Transaction + tags: + - Soroban /user/create: post: consumes: @@ -2127,4 +2213,23 @@ paths: summary: Fund Wallet tags: - Wallets + /wallets/sponsor_pk/: + get: + consumes: + - application/json + description: Get Sponsor Public Key + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/v1.response' + summary: Get Sponsor Public Key + tags: + - Wallets swagger: "2.0" diff --git a/backend/go.mod b/backend/go.mod index ac9f2abc..408d7d28 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -17,7 +17,7 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.2 - golang.org/x/crypto v0.14.0 + golang.org/x/crypto v0.15.0 ) require ( @@ -48,6 +48,7 @@ require ( github.com/invopop/jsonschema v0.12.0 // indirect github.com/itchyny/gojq v0.12.13 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/pp v3.0.1+incompatible // indirect @@ -70,8 +71,8 @@ require ( go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.5.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/protobuf v1.31.0 // indirect @@ -80,6 +81,7 @@ require ( require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/Eun/go-hit v0.5.23 + github.com/aws/aws-sdk-go v1.48.0 github.com/bitly/go-notify v0.0.0-20130217044602-0a148b8111d6 github.com/sirupsen/logrus v1.9.3 github.com/vearne/gin-timeout v0.1.7 diff --git a/backend/go.sum b/backend/go.sum index 895e91b9..bcc4340c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -30,6 +30,8 @@ github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZ github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= +github.com/aws/aws-sdk-go v1.48.0 h1:1SeJ8agckRDQvnSCt1dGZYAwUaoD2Ixj6IaXB4LCv8Q= +github.com/aws/aws-sdk-go v1.48.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bitly/go-notify v0.0.0-20130217044602-0a148b8111d6 h1:ybynk3s3i+9K288wkO0umbIyMSEE+MktDHFLfLo2yEc= @@ -188,6 +190,10 @@ github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+ github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -330,8 +336,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -394,8 +400,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -406,8 +412,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 2a728d97..1a6c3947 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -15,15 +15,18 @@ import ( "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/repo" + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase/service" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/httpserver" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/logger" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/postgres" + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/storage" "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 entity.ProducerInterface, tRepo *toml.DefaultTomlGenerator) { +func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface, tRepo *toml.DefaultTomlGenerator, storageService storage.StorageService) { l := logger.New(cfg.Log.Level) + // Use cases authUc := usecase.NewAuthUseCase( repo.New(pg), cfg.JWT.SecretKey, @@ -34,13 +37,16 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.Produ walletUc := usecase.NewWalletUseCase( repo.NewWalletRepo(pg), ) + assetUc := usecase.NewAssetUseCase( repo.NewAssetRepo(pg), repo.NewWalletRepo(pg), tRepo, repo.NewTomlRepo(pg), cfg.Horizon, + service.NewAssetService(storageService), ) + roleUc := usecase.NewRoleUseCase( repo.NewRoleRepo(pg), ) @@ -66,7 +72,7 @@ func Run(cfg *config.Config, pg *postgres.Postgres, pKp, pHor, pEnv entity.Produ handler := gin.Default() handler.Use(timeout.Timeout(timeout.WithTimeout(50 * time.Second))) - v1.NewRouter(handler, pKp, pHor, pEnv, *authUc, *userUc, *walletUc, *assetUc, *roleUc, *rolePermissionUc, *vaultCategoryUc, *vaultUc, *contractUc, *logUc, cfg.HTTP, l) + v1.NewRouter(handler, pKp, pHor, pEnv, pSub, pSig, *authUc, *userUc, *walletUc, *assetUc, *roleUc, *rolePermissionUc, *vaultCategoryUc, *vaultUc, *contractUc, *logUc, cfg.HTTP, l) 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 6ed8a063..cba82e53 100644 --- a/backend/internal/controller/http/v1/assets.go +++ b/backend/internal/controller/http/v1/assets.go @@ -52,6 +52,7 @@ func newAssetsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, as useca h.POST("/generate-toml", r.generateTOML) h.PUT("/update-toml", r.updateTOML) h.GET("/toml-data", r.getTomlData) + h.PUT("/:id/update-contract-id", r.updateContractId) h.GET("/:id/image.png", r.getAssetImage) } } @@ -116,6 +117,15 @@ type UploadAssetImageRequest struct { Image string `json:"image" example:"iVBORw0KGgoAAAANSUhEUgAACqoAAAMMCAMAAAAWqpRaAAADAFBMVEX///..."` } +type PaginatedAssetsResponse struct { + Assets []entity.Asset `json:"assets"` + TotalPages int `json:"totalPages"` +} + +type UpdateContractIdRequest struct { + ContractId string `json:"contract_id" example:"iVBORw0KGgoAAAANSUhEUgAACqoAAAMMCAMAAAAWqpRaAAADAFBMVEX///..."` +} + // @Summary Create a new asset // @Description Create and issue a new asset on Stellar // @Tags Assets @@ -129,6 +139,8 @@ type UploadAssetImageRequest struct { // @Router /assets [post] func (r *assetsRoutes) createAsset(c *gin.Context) { var request CreateAssetRequest + var imageBytes []byte + if err := c.ShouldBindJSON(&request); err != nil { r.logger.Error(err, "http - v1 - create asset - bind") errorResponse(c, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()), err) @@ -268,7 +280,7 @@ func (r *assetsRoutes) createAsset(c *gin.Context) { } if request.Image != "" { - asset.Image, err = base64.StdEncoding.DecodeString(request.Image) + imageBytes, err = base64.StdEncoding.DecodeString(request.Image) if err != nil { r.logger.Error(err, "http - v1 - create asset - decode image") errorResponse(c, http.StatusBadRequest, "Failed to decode base64 image", err) @@ -276,7 +288,7 @@ func (r *assetsRoutes) createAsset(c *gin.Context) { } } - asset, err = r.as.Create(asset) + asset, err = r.as.Create(asset, imageBytes) if err != nil { r.logger.Error(err, "http - v1 - create asset - create asset") errorResponse(c, http.StatusNotFound, "database problems", err) @@ -854,7 +866,7 @@ func (r *assetsRoutes) updateAuthFlags(c *gin.Context) { // @Produce json // @Param page query int false "Page number" // @Param limit query int false "Number of items per page" -// @Success 200 {object} []entity.Asset +// @Success 200 {object} PaginatedAssetsResponse // @Failure 500 {object} response // @Router /assets [get] func (r *assetsRoutes) getAllAssets(c *gin.Context) { @@ -877,14 +889,17 @@ func (r *assetsRoutes) getAllAssets(c *gin.Context) { } // Fetch paginated assets - assets, err := r.as.GetPaginatedAssets(page, limit) + assets, totalPages, err := r.as.GetPaginatedAssets(page, limit) if err != nil { r.logger.Error(err, "http - v1 - get all assets - get paginated") errorResponse(c, http.StatusInternalServerError, "error getting paginated assets", err) return } - c.JSON(http.StatusOK, assets) + c.JSON(http.StatusOK, PaginatedAssetsResponse{ + Assets: assets, + TotalPages: totalPages, + }) } else { // Fetch all assets assets, err := r.as.GetAll() @@ -1070,3 +1085,30 @@ func (r *assetsRoutes) getTomlData(c *gin.Context) { c.JSON(http.StatusOK, tomlContent) } + +// @Summary Update a Contract ID +// @Description Update a Contract ID +// @Tags Assets +// @Accept json +// @Produce json +// @Param request body entity.UpdateContractIdRequest true "Contract ID" +// @Success 200 {object} entity.UpdateContractIdRequest +// @Failure 400 {object} response +// @Failure 500 {object} response +// @Router /assets/update-contract-id [put] +func (r *assetsRoutes) updateContractId(c *gin.Context) { + var request UpdateContractIdRequest + if err := c.ShouldBindJSON(&request); err != nil { + errorResponse(c, http.StatusBadRequest, "invalid request body", err) + return + } + + assetId := c.Param("id") + err := r.as.UpdateContractId(assetId, request.ContractId) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "error updating Contract ID", err) + return + } + + c.JSON(http.StatusOK, gin.H{"contract_id": request.ContractId}) +} diff --git a/backend/internal/controller/http/v1/contracts.go b/backend/internal/controller/http/v1/contracts.go index 38458da3..8b94e5de 100644 --- a/backend/internal/controller/http/v1/contracts.go +++ b/backend/internal/controller/http/v1/contracts.go @@ -17,18 +17,22 @@ type contractRoutes struct { c usecase.ContractUseCase v usecase.VaultUseCase as usecase.AssetUseCase + u usecase.UserUseCase l *logger.Logger } func newContractRoutes(handler *gin.RouterGroup, m HTTPControllerMessenger, a usecase.AuthUseCase, c usecase.ContractUseCase, v usecase.VaultUseCase, - as usecase.AssetUseCase, l *logger.Logger, + as usecase.AssetUseCase, u usecase.UserUseCase, l *logger.Logger, ) { - r := &contractRoutes{m, a, c, v, as, l} + r := &contractRoutes{m, a, c, v, as, u, l} h := handler.Group("/contract").Use(Auth(r.a.ValidateToken())) { h.GET("/:id", r.getContractById) h.POST("", r.createContract) - h.GET("list", r.getAllContracts) + h.GET("/list", r.getAllContracts) + h.GET("/history/:contractId", r.getContractHistory) + h.POST("/history/", r.addContractHistory) + h.PUT("/history/", r.updateContractHistory) } } @@ -38,9 +42,25 @@ type CreateContractRequest struct { VaultId string `json:"vault_id" binding:"required" example:"1"` Address string `json:"address" binding:"required" example:"GSDSC..."` YieldRate int `json:"yield_rate" binding:"required" example:"1"` - Term int `json:"term" binding:"required" example:"1"` + Term int `json:"term" binding:"required" example:"60"` MinDeposit int `json:"min_deposit" binding:"required" example:"1"` PenaltyRate int `json:"penalty_rate" binding:"required" example:"1"` + Compound int `json:"compound" example:"1"` +} + +type AddContractHistoryRequest struct { + DepositAmount float64 `json:"deposit_amount" binding:"required" example:"100"` + ContractId int `json:"contract_id" binding:"required" example:"GSDSC..."` +} + +type UpdateContractHistoryRequest struct { + WithdrawAmount float64 `json:"withdraw_amount" binding:"required" example:"100"` + ContractId int `json:"contract_id" binding:"required" example:"GSDSC..."` +} + +type PaginatedContractsResponse struct { + Contracts []entity.Contract `json:"contracts"` + TotalPages int `json:"totalPages"` } // @Summary Create a new contract @@ -94,6 +114,7 @@ func (r *contractRoutes) createContract(c *gin.Context) { Term: request.Term, MinDeposit: request.MinDeposit, PenaltyRate: request.PenaltyRate, + Compound: request.Compound, } contract, err = r.c.Create(contract) @@ -138,13 +159,16 @@ func (r *contractRoutes) getAllContracts(c *gin.Context) { } // Get paginated contracts - contracts, err := r.c.GetPaginatedContracts(page, limit) + contracts, totalPages, err := r.c.GetPaginatedContracts(page, limit) if err != nil { r.l.Error(err, "http - v1 - get all contracts - GetPaginatedContracts") errorResponse(c, http.StatusInternalServerError, "error getting paginated contracts", err) return } - c.JSON(http.StatusOK, contracts) + c.JSON(http.StatusOK, PaginatedContractsResponse{ + Contracts: contracts, + TotalPages: totalPages, + }) } else { // Get all contracts without pagination contracts, err := r.c.GetAll() @@ -168,18 +192,154 @@ func (r *contractRoutes) getAllContracts(c *gin.Context) { func (r *contractRoutes) getContractById(c *gin.Context) { idStr := c.Param("id") - vaultId, err := strconv.Atoi(idStr) + contract, err := r.c.GetById(idStr) if err != nil { r.l.Error(err, "http - v1 - get contract by id - Atoi") - errorResponse(c, http.StatusBadRequest, "invalid vault ID", err) + errorResponse(c, http.StatusBadRequest, "invalid contract ID", err) + return + } + + c.JSON(http.StatusOK, contract) +} + +// @Summary Get contract history +// @Description Retrieve a list of history of contract +// @Tags ContractHistory +// @Accept json +// @Produce json +// @Param page query int false "Page number for pagination" +// @Param limit query int false "Number of items per page for pagination" +// @Success 200 {object} []entity.Contract +// @Failure 400 {object} response "Invalid query parameters" +// @Failure 500 {object} response "Internal server error" +// @Router /contracts [get] +func (r *contractRoutes) getContractHistory(c *gin.Context) { + contractId := c.Param("contractId") + + contract, err := r.c.GetById(contractId) + if err != nil { + errorResponse(c, http.StatusNotFound, "contract not found", err) + } + + token := c.Request.Header.Get("Authorization") + user, err := r.a.GetUserByToken(token) + if err != nil { + errorResponse(c, http.StatusNotFound, "user not found", err) return } - contract, err := r.v.GetById(vaultId) + + userID, err := strconv.Atoi(user.ID) + contracts, err := r.c.GetHistory(userID, contract.Id) if err != nil { - r.l.Error(err, "http - v1 - get contract by id - GetById") - errorResponse(c, http.StatusInternalServerError, "error getting contract", err) + r.l.Error(err, "http - v1 - get history - GetHistory") + errorResponse(c, http.StatusInternalServerError, "error getting history", err) return } + c.JSON(http.StatusOK, contracts) +} - c.JSON(http.StatusOK, contract) +// @Summary Add contract history +// @Description Add contract history +// @Tags Contract +// @Accept json +// @Produce json +// @Param request body AddContractHistoryRequest true "History info" +// @Success 200 {object} entity.ContractHistory +// @Failure 400 {object} response +// @Failure 404 {object} response +// @Failure 500 {object} response +// @Router /contract/history [post] +func (r *contractRoutes) addContractHistory(c *gin.Context) { + var request AddContractHistoryRequest + var err error + + if err := c.ShouldBindJSON(&request); err != nil { + r.l.Error(err, "http - v1 - add contract history - ShouldBindJSON") + errorResponse(c, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()), err) + return + } + + contractId := strconv.Itoa(request.ContractId) + contract, err := r.c.GetById(contractId) + if err != nil { + r.l.Error(err, "http - v1 - add contract history - GetById") + errorResponse(c, http.StatusNotFound, "contract not found", err) + return + } + + token := c.Request.Header.Get("Authorization") + user, err := r.u.GetUserByToken(token) + if err != nil { + r.l.Error(err, "http - v1 - create contract - GetById") + errorResponse(c, http.StatusNotFound, "vault not found", err) + return + } + + contractHistory := entity.ContractHistory{ + Contract: contract, + User: user, + DepositAmount: request.DepositAmount, + } + + contractHistory, err = r.c.AddContractHistory(contractHistory) + if err != nil { + r.l.Error(err, "http - v1 - add contract history - Create") + errorResponse(c, http.StatusNotFound, fmt.Sprintf("error: %s", err.Error()), err) + return + } + + c.JSON(http.StatusOK, contractHistory) +} + +// @Summary Update contract history +// @Description Update contract history +// @Tags Contract +// @Accept json +// @Produce json +// @Param request body UpdateContractHistoryRequest true "History info" +// @Success 200 {object} entity.ContractHistory +// @Failure 400 {object} response +// @Failure 404 {object} response +// @Failure 500 {object} response +// @Router /contract/history [put] +func (r *contractRoutes) updateContractHistory(c *gin.Context) { + var request UpdateContractHistoryRequest + var err error + + if err := c.ShouldBindJSON(&request); err != nil { + r.l.Error(err, "http - v1 - add contract history - ShouldBindJSON") + errorResponse(c, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()), err) + return + } + + contractId := strconv.Itoa(request.ContractId) + contract, err := r.c.GetById(contractId) + if err != nil { + r.l.Error(err, "http - v1 - add contract history - GetById") + errorResponse(c, http.StatusNotFound, "contract not found", err) + return + } + + token := c.Request.Header.Get("Authorization") + user, err := r.u.GetUserByToken(token) + if err != nil { + r.l.Error(err, "http - v1 - create contract - GetById") + errorResponse(c, http.StatusNotFound, "vault not found", err) + return + } + + contractHistory := entity.ContractHistory{ + Contract: contract, + User: user, + WithdrawAmount: &request.WithdrawAmount, + } + + contractHistory, err = r.c.UpdateContractHistory(contractHistory) + if err != nil { + r.l.Error(err, "http - v1 - update contract history - Create") + errorResponse(c, http.StatusNotFound, fmt.Sprintf("error: %s", err.Error()), err) + return + } + + c.JSON(http.StatusOK, contractHistory) } diff --git a/backend/internal/controller/http/v1/router.go b/backend/internal/controller/http/v1/router.go index 5abacc0c..c53e1cd9 100644 --- a/backend/internal/controller/http/v1/router.go +++ b/backend/internal/controller/http/v1/router.go @@ -57,7 +57,7 @@ func CORSMiddleware(cfg config.HTTP, l *logger.Logger) gin.HandlerFunc { // @BasePath / func NewRouter( handler *gin.Engine, - pKp, pHor, pEnv entity.ProducerInterface, + pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface, authUseCase usecase.AuthUseCase, userUseCase usecase.UserUseCase, walletUseCase usecase.WalletUseCase, @@ -72,7 +72,7 @@ func NewRouter( logger *logger.Logger, ) { // Messenger - messengerController := newHTTPControllerMessenger(pKp, pHor, pEnv) + messengerController := newHTTPControllerMessenger(pKp, pHor, pEnv, pSub, pSig) // Options Gin handler.Use(gin.Logger()) handler.Use(gin.Recovery()) @@ -85,15 +85,16 @@ func NewRouter( handler.Use(CORSMiddleware(cfg, logger)) groupV1 := handler.Group("/v1") { - newUserRoutes(groupV1, userUseCase, authUseCase, rolePermissionUc, logger) + newUserRoutes(groupV1, userUseCase, authUseCase, rolePermissionUc, logger, vaultUc) newWalletsRoutes(groupV1, walletUseCase, messengerController, authUseCase, logger) newAssetsRoutes(groupV1, walletUseCase, assetUseCase, messengerController, authUseCase, logUc, logger) newRoleRoutes(groupV1, roleUseCase, messengerController, logger) newRolePermissionsRoutes(groupV1, rolePermissionUc, messengerController, logger) newVaultCategoryRoutes(groupV1, messengerController, authUseCase, vaultCategoryUc, logger) newVaultRoutes(groupV1, messengerController, authUseCase, vaultUc, vaultCategoryUc, walletUseCase, assetUseCase, logger) - newContractRoutes(groupV1, messengerController, authUseCase, contractUc, vaultUc, assetUseCase, logger) + newContractRoutes(groupV1, messengerController, authUseCase, contractUc, vaultUc, assetUseCase, userUseCase, logger) newLogTransactionsRoutes(groupV1, walletUseCase, assetUseCase, messengerController, logUc, authUseCase, logger) + newSorobanRoutes(groupV1, walletUseCase, messengerController, authUseCase) newAssetTomlRoutes(groupV1, walletUseCase, assetUseCase, messengerController, authUseCase, logUc, logger) } } diff --git a/backend/internal/controller/http/v1/soroban.go b/backend/internal/controller/http/v1/soroban.go new file mode 100644 index 00000000..d73723e0 --- /dev/null +++ b/backend/internal/controller/http/v1/soroban.go @@ -0,0 +1,100 @@ +package v1 + +import ( + "fmt" + "net/http" + + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" + "github.com/CheesecakeLabs/token-factory-v2/backend/internal/usecase" + "github.com/gin-gonic/gin" +) + +type sorobanRoutes struct { + w usecase.WalletUseCase + m HTTPControllerMessenger + a usecase.AuthUseCase +} + +func newSorobanRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, m HTTPControllerMessenger, a usecase.AuthUseCase) { + r := &sorobanRoutes{w, m, a} + + h := handler.Group("/soroban-transactions") + h.Use(Auth(r.a.ValidateToken())) + { + h.POST("/sign", r.signTransaction) + h.POST("/submit", r.submitTransaction) + } +} + +type SignedTransactionRequest struct { + Envelope string `json:"envelope" binding:"required" example:"KJDSKD..."` + WalletPk string `json:"wallet_pk" example:"GDSKJG..."` +} + +type SubmitTransactionRequest struct { + Envelope string `json:"envelope" binding:"required" example:"KJDSKD..."` +} + +// @Summary Signed Transaction +// @Description Signed a XDR transaction +// @Tags Soroban +// @Accept json +// @Produce json +// @Param request body SignedTransactionRequest true "Signed a XDR transaction" +// @Success 200 {object} response +// @Failure 400 {object} response +// @Failure 500 {object} response +// @Router /soroban-transactions/sign [post] +func (r *sorobanRoutes) signTransaction(c *gin.Context) { + var request SignedTransactionRequest + if err := c.ShouldBindJSON(&request); err != nil { + errorResponse(c, http.StatusBadRequest, "invalid request", err) + return + } + + if request.WalletPk == "" { + sponsor, err := r.w.Get(_sponsorId) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "database problems", err) + return + } + + request.WalletPk = sponsor.Key.PublicKey + } + res, err := r.m.SendMessage(entity.SignChannel, entity.SignTransactionRequest{ + PublicKeys: []string{request.WalletPk}, + Envelope: request.Envelope, + }) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "messaging problems", err) + return + } + + c.JSON(http.StatusOK, res) +} + +// @Summary Submit Transaction +// @Description Submit a XDR transaction +// @Tags Soroban +// @Accept json +// @Produce json +// @Param request body SubmitTransactionRequest true "Submit a XDR transaction" +// @Success 200 {object} response +// @Failure 400 {object} response +// @Failure 500 {object} response +// @Router /soroban-transactions/submit [post] +func (r *sorobanRoutes) submitTransaction(c *gin.Context) { + var request SubmitTransactionRequest + if err := c.ShouldBindJSON(&request); err != nil { + errorResponse(c, http.StatusBadRequest, "invalid request", err) + return + } + res, err := r.m.SendMessage(entity.SubmitTransactionChannel, entity.SubmitRequest{Envelope: request.Envelope}) + if err != nil { + fmt.Println(err) + errorResponse(c, http.StatusInternalServerError, "messaging problems", err) + return + } + + c.JSON(http.StatusOK, res) +} diff --git a/backend/internal/controller/http/v1/users.go b/backend/internal/controller/http/v1/users.go index 3df336a1..3f8da692 100644 --- a/backend/internal/controller/http/v1/users.go +++ b/backend/internal/controller/http/v1/users.go @@ -15,17 +15,20 @@ type usersRoutes struct { t usecase.UserUseCase a usecase.AuthUseCase rP usecase.RolePermissionUseCase - l logger.Interface + v usecase.VaultUseCase + // l logger.Interface + l logger.Interface } -func newUserRoutes(handler *gin.RouterGroup, t usecase.UserUseCase, a usecase.AuthUseCase, rP usecase.RolePermissionUseCase, l logger.Interface) { - r := &usersRoutes{t, a, rP, l} +func newUserRoutes(handler *gin.RouterGroup, t usecase.UserUseCase, a usecase.AuthUseCase, rP usecase.RolePermissionUseCase, l logger.Interface, v usecase.VaultUseCase) { + r := &usersRoutes{t, a, rP, v, l} h := handler.Group("/users") { h.POST("/create", r.createUser) h.POST("/login", r.autentication) h.POST("/logout", r.logout) + h.POST("/forget-password", r.forgetPassword) secured := h.Group("/").Use(Auth(a.ValidateToken())) { @@ -141,13 +144,15 @@ func (r *usersRoutes) autentication(c *gin.Context) { // @Failure 500 {object} response // @Router /user/logout [post] func (r *usersRoutes) logout(c *gin.Context) { - var user entity.User - if err := c.ShouldBindJSON(&user); err != nil { - r.l.Error(err, "http - v1 - logout - ShouldBindJSON") - errorResponse(c, http.StatusBadRequest, "invalid request body", err) + token := c.GetHeader("Authorization") + user, err := r.t.GetUserByToken(token) + if err != nil { + r.l.Error(err, "http - v1 - getUserByToken - GetUserByToken") + errorResponse(c, http.StatusInternalServerError, "database problems", err) return } - err := r.a.UpdateToken(user.Email, "") + + err = r.a.UpdateToken(user.ID, user.ID) if err != nil { r.l.Error(err, "http - v1 - logout - UpdateToken") errorResponse(c, http.StatusInternalServerError, "error updating token", err) @@ -218,5 +223,26 @@ func (r *usersRoutes) getProfile(c *gin.Context) { return } + if profile.VaultId != nil { + vault, err := r.v.GetById(*profile.VaultId) + if err != nil { + fmt.Println(err) + return + } + + profile.Vault = &vault + } + c.JSON(http.StatusOK, profile) } + +// @Summary Forget Password +// @Description Forget Password +// @Schemes +// @Tags user +// @Accept json +// @Produce json +// @Success 200 {object} entity. +// @Router /users [get] +func (r *usersRoutes) forgetPassword(c *gin.Context) { +} diff --git a/backend/internal/controller/http/v1/utils.go b/backend/internal/controller/http/v1/utils.go index 32f69d8d..b2fd1b6e 100644 --- a/backend/internal/controller/http/v1/utils.go +++ b/backend/internal/controller/http/v1/utils.go @@ -16,10 +16,12 @@ type HTTPControllerMessenger struct { pKp entity.ProducerInterface pHor entity.ProducerInterface pEnv entity.ProducerInterface + pSub entity.ProducerInterface + pSig entity.ProducerInterface } -func newHTTPControllerMessenger(pKp, pHor, pEnv entity.ProducerInterface) HTTPControllerMessenger { - return HTTPControllerMessenger{pKp, pHor, pEnv} +func newHTTPControllerMessenger(pKp, pHor, pEnv, pSub, pSig entity.ProducerInterface) HTTPControllerMessenger { + return HTTPControllerMessenger{pKp, pHor, pEnv, pSub, pSig} } func (m *HTTPControllerMessenger) SendMessage(chanName string, value interface{}) (*entity.NotifyData, error) { @@ -27,7 +29,6 @@ func (m *HTTPControllerMessenger) SendMessage(chanName string, value interface{} if err != nil { return &entity.NotifyData{}, fmt.Errorf("sendMessage - generateHash: %v", err) } - channel := make(chan interface{}) notify.Start(msgKey, channel) @@ -80,6 +81,10 @@ func (m *HTTPControllerMessenger) produce(chanName string, msgKey string, value err = m.pHor.Produce(msgKey, value) case entity.EnvelopeChannel: err = m.pEnv.Produce(msgKey, value) + case entity.SubmitTransactionChannel: + err = m.pSub.Produce(msgKey, value) + case entity.SignChannel: + err = m.pSig.Produce(msgKey, value) default: err = fmt.Errorf("invalid channel name") } diff --git a/backend/internal/controller/http/v1/utils_test.go b/backend/internal/controller/http/v1/utils_test.go index 80c75aca..1b7f1532 100644 --- a/backend/internal/controller/http/v1/utils_test.go +++ b/backend/internal/controller/http/v1/utils_test.go @@ -29,7 +29,7 @@ func (p *mockProducer) Produce(key string, value interface{}) error { func TestSendMessage(t *testing.T) { mockProducer := newMockProducer() - messenger := newHTTPControllerMessenger(mockProducer, mockProducer, mockProducer) + messenger := newHTTPControllerMessenger(mockProducer, mockProducer, mockProducer, mockProducer, mockProducer) reqData := &entity.CreateKeypairRequest{Amount: 1} actualData, err := messenger.SendMessage(entity.EnvelopeChannel, reqData) diff --git a/backend/internal/controller/http/v1/vault.go b/backend/internal/controller/http/v1/vault.go index cd0e31ae..8f59a713 100644 --- a/backend/internal/controller/http/v1/vault.go +++ b/backend/internal/controller/http/v1/vault.go @@ -38,8 +38,9 @@ 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"` + VaultCategoryId *int `json:"vault_category_id" example:"1"` AssetsId []int `json:"assets_id" binding:"required"` + OwnerId *int `json:"owner_id"` } type UpdateVaultCategoryRequest struct { @@ -54,6 +55,11 @@ type UpdateVaultAssetRequest struct { IsRemove bool `json:"is_remove" example:"false"` } +type PaginatedVaultsResponse struct { + Vaults []entity.Vault `json:"vaults"` + TotalPages int `json:"totalPages"` +} + // @Summary Create a new vault // @Description Create and issue a new asset on Stellar // @Tags Vault @@ -75,11 +81,15 @@ func (r *vaultRoutes) createVault(c *gin.Context) { return } - vaultCategory, err := r.vc.GetById(request.VaultCategoryId) - if err != nil { - r.l.Error(err, "http - v1 - create vault - vault category") - errorResponse(c, http.StatusNotFound, "source wallet not found", err) - return + var vaultCategory entity.VaultCategory + + if request.VaultCategoryId != nil { + vaultCategory, err = r.vc.GetById(*request.VaultCategoryId) + if err != nil { + r.l.Error(err, "http - v1 - create vault - vault category") + errorResponse(c, http.StatusNotFound, "vault category not found", err) + return + } } sponsorID := _sponsorId @@ -106,16 +116,34 @@ func (r *vaultRoutes) createVault(c *gin.Context) { } walletPk := kpRes.PublicKeys[0] - ops := []entity.Operation{ - { - Type: entity.CreateAccountOp, - Target: walletPk, - Amount: _startingBalance, - Origin: sponsor.Key.PublicKey, - Sponsor: sponsor.Key.PublicKey, + wallet, err := r.w.Create(entity.Wallet{ + Type: entity.DistributorType, + Key: entity.Key{ + PublicKey: walletPk, + Weight: 1, }, + }) + + fundRes, err := r.m.SendMessage(entity.HorizonChannel, entity.HorizonRequest{ + Id: wallet.Id, + Type: "fundWithFriendbot", + Account: walletPk, + }) + if err != nil { + r.l.Error(err, "http - v1 - fund wallet - SendMessage") + errorResponse(c, http.StatusInternalServerError, "messaging problems", err) + } + + fundResult := fundRes.Message.(entity.HorizonResponse) + + if fundResult.StatusCode != 200 { + r.l.Error(err, "http - v1 - fund wallet - fundRes.StatusCode != 200") + errorResponse(c, http.StatusInternalServerError, "friendbot error", err) + return } + ops := []entity.Operation{} + for _, assetId := range request.AssetsId { asset, err := r.as.GetById(strconv.Itoa(assetId)) if err != nil { @@ -135,38 +163,34 @@ func (r *vaultRoutes) createVault(c *gin.Context) { }) } - Id := generateID() - res, err = r.m.SendMessage(entity.EnvelopeChannel, entity.EnvelopeRequest{ - Id: Id, - MainSource: sponsor.Key.PublicKey, - PublicKeys: []string{sponsor.Key.PublicKey, walletPk}, - Operations: ops, - }) - if err != nil { - r.l.Error(err, fmt.Sprintf("http - v1 - create vault - send message %d", Id)) - errorResponse(c, http.StatusInternalServerError, "starlabs messaging problems", err) - return - } - _, ok = res.Message.(entity.EnvelopeResponse) - if !ok { - r.l.Error(err, "http - v1 - create vault - Parse Envelope Response") - errorResponse(c, http.StatusInternalServerError, "unexpected starlabs response", err) - return - } + if len(ops) > 0 { + Id := generateID() + res, err = r.m.SendMessage(entity.EnvelopeChannel, entity.EnvelopeRequest{ + Id: Id, + MainSource: sponsor.Key.PublicKey, + PublicKeys: []string{sponsor.Key.PublicKey, walletPk}, + Operations: ops, + }) - wallet := entity.Wallet{ - Type: entity.DistributorType, - Funded: true, - Key: entity.Key{ - PublicKey: walletPk, - Weight: 1, - }, + if err != nil { + r.l.Error(err, fmt.Sprintf("http - v1 - create vault - send message %d", Id)) + errorResponse(c, http.StatusInternalServerError, "starlabs messaging problems", err) + return + } + + _, ok = res.Message.(entity.EnvelopeResponse) + if !ok { + r.l.Error(err, "http - v1 - create vault - Parse Envelope Response") + errorResponse(c, http.StatusInternalServerError, "unexpected starlabs response", err) + return + } } vault := entity.Vault{ Name: request.Name, - VaultCategory: vaultCategory, + VaultCategory: &vaultCategory, Wallet: wallet, + OwnerId: request.OwnerId, } vault, err = r.v.Create(vault) @@ -193,6 +217,7 @@ func (r *vaultRoutes) createVault(c *gin.Context) { func (r *vaultRoutes) getAllVaults(c *gin.Context) { pageQuery := c.Query("page") limitQuery := c.Query("limit") + isAll := c.Query("all") == "all" // Check if pagination parameters are provided if pageQuery != "" && limitQuery != "" { @@ -211,16 +236,19 @@ func (r *vaultRoutes) getAllVaults(c *gin.Context) { } // Get paginated vaults - vaults, err := r.v.GetPaginatedVaults(page, limit) + vaults, totalPages, err := r.v.GetPaginatedVaults(page, limit) if err != nil { r.l.Error(err, "http - v1 - get all vaults - GetPaginatedVaults") errorResponse(c, http.StatusInternalServerError, "error getting paginated vaults", err) return } - c.JSON(http.StatusOK, vaults) + c.JSON(http.StatusOK, PaginatedVaultsResponse{ + Vaults: vaults, + TotalPages: totalPages, + }) } else { // Get all vaults without pagination - vaults, err := r.v.GetAll() + vaults, err := r.v.GetAll(isAll) if err != nil { r.l.Error(err, "http - v1 - get all vaults - GetAll") errorResponse(c, http.StatusInternalServerError, "error getting all vaults", err) diff --git a/backend/internal/controller/http/v1/wallets.go b/backend/internal/controller/http/v1/wallets.go index 6897899a..9cccccc8 100644 --- a/backend/internal/controller/http/v1/wallets.go +++ b/backend/internal/controller/http/v1/wallets.go @@ -23,6 +23,7 @@ func newWalletsRoutes(handler *gin.RouterGroup, w usecase.WalletUseCase, m HTTPC h.GET("", r.list) h.POST("", r.create) h.POST("fund/", r.fundWallet) + h.GET("sponsor_pk/", r.getSponsorPublicKey) } } @@ -158,3 +159,21 @@ func (r *walletsRoutes) fundWallet(c *gin.Context) { c.JSON(http.StatusOK, wallet) } + +// @Summary Get Sponsor Public Key +// @Description Get Sponsor Public Key +// @Tags Wallets +// @Accept json +// @Produce json +// @Success 200 {object} string +// @Failure 500 {object} response +// @Router /wallets/sponsor_pk/ [get] +func (r *walletsRoutes) getSponsorPublicKey(c *gin.Context) { + sponsor, err := r.w.Get(_sponsorId) + if err != nil { + errorResponse(c, http.StatusInternalServerError, "database problems", err) + return + } + + c.JSON(http.StatusOK, sponsor.Key.PublicKey) +} diff --git a/backend/internal/entity/asset.go b/backend/internal/entity/asset.go index 66eb9568..82d8ab3f 100644 --- a/backend/internal/entity/asset.go +++ b/backend/internal/entity/asset.go @@ -1,14 +1,15 @@ package entity type Asset struct { - Id int `json:"id" example:"1"` - Name string `json:"name" example:"USD Coin"` - Code string `json:"code" example:"USDC"` - Distributor Wallet `json:"distributor"` - Issuer Wallet `json:"issuer"` - Amount int `json:"amount" example:"1000000"` - AssetType string `json:"asset_type"` - Image []byte `json:"image,omitempty"` + Id int `json:"id" example:"1"` + Name string `json:"name" example:"USD Coin"` + Code string `json:"code" example:"USDC"` + Distributor Wallet `json:"distributor"` + Issuer Wallet `json:"issuer"` + Amount int `json:"amount" example:"1000000"` + AssetType string `json:"asset_type"` + Image string `json:"image,omitempty"` + ContractId *string `json:"contract_id,omitempty"` } const ( diff --git a/backend/internal/entity/channel.go b/backend/internal/entity/channel.go index fa5f1df9..3e7a3bb9 100644 --- a/backend/internal/entity/channel.go +++ b/backend/internal/entity/channel.go @@ -1,7 +1,9 @@ package entity var ( - EnvelopeChannel = "EnvelopeChannel" - HorizonChannel = "HorizonChannel" - CreateKeypairChannel = "CreateKeypairChannel" + EnvelopeChannel = "EnvelopeChannel" + HorizonChannel = "HorizonChannel" + CreateKeypairChannel = "CreateKeypairChannel" + SubmitTransactionChannel = "SubmitTransactionChannel" + SignChannel = "SignChannel" ) diff --git a/backend/internal/entity/contract.go b/backend/internal/entity/contract.go index 00e0967d..9ea0115e 100644 --- a/backend/internal/entity/contract.go +++ b/backend/internal/entity/contract.go @@ -11,4 +11,5 @@ type Contract struct { MinDeposit int `json:"min_deposit" db:"min_deposit"` PenaltyRate int `json:"penalty_rate" db:"penalty_rate"` CreatedAt string `json:"created_at" db:"created_at"` + Compound int `json:"compound" db:"compound"` } diff --git a/backend/internal/entity/contract_history.go b/backend/internal/entity/contract_history.go new file mode 100644 index 00000000..276b3794 --- /dev/null +++ b/backend/internal/entity/contract_history.go @@ -0,0 +1,11 @@ +package entity + +type ContractHistory struct { + Id int `json:"id" example:"1"` + Contract Contract `json:"contract"` + DepositedAt *string `json:"deposited_at"` + DepositAmount float64 `json:"deposit_amount"` + WithdrawnAt *string `json:"withdrawn_at"` + WithdrawAmount *float64 `json:"withdraw_amount"` + User User `json:"user"` +} diff --git a/backend/internal/entity/message.go b/backend/internal/entity/message.go index 20eefb52..d872ea8f 100644 --- a/backend/internal/entity/message.go +++ b/backend/internal/entity/message.go @@ -75,4 +75,21 @@ type ( Account string Weight uint8 } + + SignTransactionRequest struct { + Id int `json:"id"` + Envelope string `json:"envelope"` + PublicKeys []string `json:"publicKeys"` + Hash string `json:"hash"` + } + + SorobanTransactionResponse struct { + Id int `json:"id"` + Envelope string `json:"envelope"` + } + + SubmitRequest struct { + Id int `json:"id"` + Envelope string `json:"envelope"` + } ) diff --git a/backend/internal/entity/transactions.go b/backend/internal/entity/transactions.go index 0e6b9599..7b728b44 100644 --- a/backend/internal/entity/transactions.go +++ b/backend/internal/entity/transactions.go @@ -10,4 +10,6 @@ const ( SetTrustLineFlagsOp = "setTrustLineFlags" FundWithFriendbotHor = "fundWithFriendbot" ClawbackOp = "clawback" + SubmitTransaction = "submitTransaction" + SignTransaction = "signTransaction" ) diff --git a/backend/internal/entity/users.go b/backend/internal/entity/users.go index 18e181cf..e57ac613 100644 --- a/backend/internal/entity/users.go +++ b/backend/internal/entity/users.go @@ -18,6 +18,8 @@ type ( Role string `json:"role" db:"role"` Email string `json:"email" db:"email"` RoleId int `json:"role_id" db:"role_id"` + VaultId *int `json:"vault_id"` + Vault *Vault `json:"vault,omitempty"` } UserRole struct { diff --git a/backend/internal/entity/vault.go b/backend/internal/entity/vault.go index 35bd374b..8dafb358 100644 --- a/backend/internal/entity/vault.go +++ b/backend/internal/entity/vault.go @@ -1,9 +1,11 @@ package entity type Vault struct { - Id int `json:"id" example:"1"` - Name string `json:"name" example:"Treasury"` - Wallet Wallet `json:"wallet"` - VaultCategory VaultCategory `json:"vault_category"` - Active int `json:"active"` + Id int `json:"id" example:"1"` + Name string `json:"name" example:"Treasury"` + Wallet Wallet `json:"wallet"` + VaultCategoryId *int `json:"vault_category_id"` + VaultCategory *VaultCategory `json:"vault_category"` + Active int `json:"active"` + OwnerId *int `json:"owner_id"` } diff --git a/backend/internal/usecase/assets.go b/backend/internal/usecase/assets.go index 0a5fb611..5f8bacf4 100644 --- a/backend/internal/usecase/assets.go +++ b/backend/internal/usecase/assets.go @@ -8,24 +8,26 @@ import ( ) type AssetUseCase struct { - aRepo AssetRepoInterface - wRepo WalletRepoInterface - tInt TomlInterface - tRepo TomlRepoInterface - cfg config.Horizon + aRepo AssetRepoInterface + wRepo WalletRepoInterface + tInt TomlInterface + tRepo TomlRepoInterface + cfg config.Horizon + storageService AssetServiceInterface } -func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface, tInt TomlInterface, tRepo TomlRepoInterface, cfg config.Horizon) *AssetUseCase { +func NewAssetUseCase(aRepo AssetRepoInterface, wRepo WalletRepoInterface, tInt TomlInterface, tRepo TomlRepoInterface, cfg config.Horizon, storageService AssetServiceInterface) *AssetUseCase { return &AssetUseCase{ - aRepo: aRepo, - wRepo: wRepo, - tInt: tInt, - tRepo: tRepo, - cfg: cfg, + aRepo: aRepo, + wRepo: wRepo, + tInt: tInt, + tRepo: tRepo, + cfg: cfg, + storageService: storageService, } } -func (uc *AssetUseCase) Create(data entity.Asset) (entity.Asset, error) { +func (uc *AssetUseCase) Create(data entity.Asset, imageBytes []byte) (entity.Asset, error) { issuer, err := uc.wRepo.CreateWalletWithKey(data.Issuer) if err != nil { return entity.Asset{}, fmt.Errorf("AssetUseCase - Create - uc.repo.CreateWalletWithKey(issuer): %w", err) @@ -38,6 +40,15 @@ func (uc *AssetUseCase) Create(data entity.Asset) (entity.Asset, error) { } data.Distributor.Id = dist.Id + // Upload image to S3 if exists + if len(imageBytes) > 0 { + assetImage, err := uc.storageService.UploadFile(fmt.Sprint(data.Id), imageBytes) + if err != nil { + return entity.Asset{}, fmt.Errorf("AssetUseCase - Create - uc.awsService.UploadAssetImage: %w", err) + } + data.Image = assetImage + } + asset, err := uc.aRepo.CreateAsset(data) if err != nil { return entity.Asset{}, fmt.Errorf("AssetUseCase - Create - uc.repo.CreateWallet: %w", err) @@ -74,7 +85,12 @@ func (uc *AssetUseCase) GetAll() ([]entity.Asset, error) { } func (uc *AssetUseCase) UploadImage(assetId string, imageBytes []byte) error { - err := uc.aRepo.StoreAssetImage(assetId, imageBytes) + assetImage, err := uc.storageService.UploadFile(assetId, imageBytes) + if err != nil { + return fmt.Errorf("AssetUseCase - Create - uc.awsService.UploadAssetImage: %w", err) + } + + err = uc.aRepo.StoreAssetImage(assetId, assetImage) if err != nil { return fmt.Errorf("ImageUseCase - UploadImage - uc.aRepo.StoreAssetImage: %w", err) } @@ -160,11 +176,19 @@ func (uc *AssetUseCase) GetTomlData() (entity.TomlData, error) { return tomParsed, err } -func (uc *AssetUseCase) GetPaginatedAssets(page int, limit int) ([]entity.Asset, error) { - assets, err := uc.aRepo.GetPaginatedAssets(page, limit) +func (uc *AssetUseCase) UpdateContractId(assetId string, contractId string) error { + err := uc.aRepo.UpdateContractId(assetId, contractId) if err != nil { - return nil, fmt.Errorf("AssetUseCase - GetPaginated - uc.repo.GetPaginated: %w", err) + return fmt.Errorf("AssetUseCase - UpdateContractId - uc.aRepo.UpdateContractId: %w", err) } + return nil +} - return assets, nil +func (uc *AssetUseCase) GetPaginatedAssets(page int, limit int) ([]entity.Asset, int, error) { + assets, totalPages, err := uc.aRepo.GetPaginatedAssets(page, limit) + if err != nil { + return nil, 0, fmt.Errorf("AssetUseCase - GetPaginated - uc.repo.GetPaginated: %w", err) + } + + return assets, totalPages, nil } diff --git a/backend/internal/usecase/assets_test.go b/backend/internal/usecase/assets_test.go index 426c2ed3..03514199 100644 --- a/backend/internal/usecase/assets_test.go +++ b/backend/internal/usecase/assets_test.go @@ -36,7 +36,8 @@ func asset(t *testing.T) (*usecase.AssetUseCase, *mocks.MockAssetRepoInterface, ra := mocks.NewMockAssetRepoInterface(mockCtl) tg := mocks.NewMockTomlInterface(mockCtl) tr := mocks.NewMockTomlRepoInterface(mockCtl) - u := usecase.NewAssetUseCase(ra, rw, tg, tr, config.Horizon{}) + st := mocks.NewMockAssetServiceInterface(mockCtl) + u := usecase.NewAssetUseCase(ra, rw, tg, tr, config.Horizon{}, st) return u, ra, rw, tg } @@ -156,7 +157,7 @@ func TestAssetUseCaseCreate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.mock() - res, err := u.Create(tc.req.(entity.Asset)) + res, err := u.Create(tc.req.(entity.Asset), []byte{}) require.EqualValues(t, tc.res, res) if tc.err == nil { diff --git a/backend/internal/usecase/contract.go b/backend/internal/usecase/contract.go index 2e779a91..01397485 100644 --- a/backend/internal/usecase/contract.go +++ b/backend/internal/usecase/contract.go @@ -35,19 +35,46 @@ func (uc *ContractUseCase) GetAll() ([]entity.Contract, error) { } func (uc *ContractUseCase) GetById(id string) (entity.Contract, error) { - asset, err := uc.cRepo.GetContractById(id) + contract, err := uc.cRepo.GetContractById(id) if err != nil { - return entity.Contract{}, fmt.Errorf("ContractUseCase - Get - uc.repo.GetContractById: %w", err) + return entity.Contract{}, fmt.Errorf("ContractUseCase - GetById - uc.repo.GetContractById: %w", err) } - return asset, nil + return contract, nil } -func (uc *ContractUseCase) GetPaginatedContracts(page int, limit int) ([]entity.Contract, error) { - contracts, err := uc.cRepo.GetPaginatedContracts(page, limit) +func (uc *ContractUseCase) GetPaginatedContracts(page int, limit int) ([]entity.Contract, int, error) { + contracts, totalPages, err := uc.cRepo.GetPaginatedContracts(page, limit) if err != nil { - return nil, fmt.Errorf("ContractUseCase - GetPaginatedContracts - uc.repo.GetPaginatedContracts: %w", err) + return nil, 0, fmt.Errorf("ContractUseCase - GetPaginatedContracts - uc.repo.GetPaginatedContracts: %w", err) } - return contracts, nil + return contracts, totalPages, nil +} + +func (uc *ContractUseCase) GetHistory(userId int, contractId int) ([]entity.ContractHistory, error) { + contractsHistory, err := uc.cRepo.GetHistory(userId, contractId) + if err != nil { + return nil, fmt.Errorf("ContractUseCase - GetHistory - uc.repo.GetHistory: %w", err) + } + + return contractsHistory, nil +} + +func (uc *ContractUseCase) AddContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) { + contractsHistory, err := uc.cRepo.AddContractHistory(contractHistory) + if err != nil { + return entity.ContractHistory{}, fmt.Errorf("ContractUseCase - AddContractHistory - uc.repo.AddContractHistory: %w", err) + } + + return contractsHistory, nil +} + +func (uc *ContractUseCase) UpdateContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) { + contractHistory, err := uc.cRepo.UpdateContractHistory(contractHistory) + if err != nil { + return entity.ContractHistory{}, fmt.Errorf("ContractUseCase - UpdateContractHistory - uc.repo.UpdateContractHistory: %w", err) + } + + return contractHistory, nil } diff --git a/backend/internal/usecase/interfaces.go b/backend/internal/usecase/interfaces.go index 4497716a..bd8bbfe5 100644 --- a/backend/internal/usecase/interfaces.go +++ b/backend/internal/usecase/interfaces.go @@ -47,9 +47,10 @@ type ( GetAssetByCode(string) (entity.Asset, error) CreateAsset(entity.Asset) (entity.Asset, error) GetAssetById(string) (entity.Asset, error) - StoreAssetImage(string, []byte) error + StoreAssetImage(string, string) error GetAssetImage(string) ([]byte, error) - GetPaginatedAssets(int, int) ([]entity.Asset, error) + GetPaginatedAssets(int, int) ([]entity.Asset, int, error) + UpdateContractId(string, string) error } // Role -. @@ -89,19 +90,22 @@ type ( } VaultRepoInterface interface { - GetVaults() ([]entity.Vault, error) + GetVaults(isAll bool) ([]entity.Vault, error) CreateVault(entity.Vault) (entity.Vault, error) UpdateVault(entity.Vault) (entity.Vault, error) GetVaultById(id int) (entity.Vault, error) DeleteVault(entity.Vault) (entity.Vault, error) - GetPaginatedVaults(int, int) ([]entity.Vault, error) + GetPaginatedVaults(int, int) ([]entity.Vault, int, error) } ContractRepoInterface interface { GetContracts() ([]entity.Contract, error) CreateContract(entity.Contract) (entity.Contract, error) GetContractById(id string) (entity.Contract, error) - GetPaginatedContracts(int, int) ([]entity.Contract, error) + GetPaginatedContracts(int, int) ([]entity.Contract, int, error) + GetHistory(userId int, contractId int) ([]entity.ContractHistory, error) + AddContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) + UpdateContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) } LogTransactionRepoInterface interface { @@ -116,4 +120,9 @@ type ( SumLogTransactionSupply(timeRange string, timeFrame time.Duration) ([]entity.SumLogTransactionSupply, error) LogTransactionSupplyByAssetID(assetID int, timeRange string, periodInitial string, interval string) (entity.LogTransactionSupply, error) } + + // Asset Service + AssetServiceInterface interface { + UploadFile(string, []byte) (string, error) + } ) diff --git a/backend/internal/usecase/mocks/mocks.go b/backend/internal/usecase/mocks/mocks.go index 58df6b25..5033d321 100644 --- a/backend/internal/usecase/mocks/mocks.go +++ b/backend/internal/usecase/mocks/mocks.go @@ -462,12 +462,13 @@ func (mr *MockAssetRepoInterfaceMockRecorder) GetAssets() *gomock.Call { } // GetPaginatedAssets mocks base method. -func (m *MockAssetRepoInterface) GetPaginatedAssets(arg0, arg1 int) ([]entity.Asset, error) { +func (m *MockAssetRepoInterface) GetPaginatedAssets(arg0, arg1 int) ([]entity.Asset, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPaginatedAssets", arg0, arg1) ret0, _ := ret[0].([]entity.Asset) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetPaginatedAssets indicates an expected call of GetPaginatedAssets. @@ -477,7 +478,7 @@ func (mr *MockAssetRepoInterfaceMockRecorder) GetPaginatedAssets(arg0, arg1 inte } // StoreAssetImage mocks base method. -func (m *MockAssetRepoInterface) StoreAssetImage(arg0 string, arg1 []byte) error { +func (m *MockAssetRepoInterface) StoreAssetImage(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "StoreAssetImage", arg0, arg1) ret0, _ := ret[0].(error) @@ -490,6 +491,20 @@ func (mr *MockAssetRepoInterfaceMockRecorder) StoreAssetImage(arg0, arg1 interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreAssetImage", reflect.TypeOf((*MockAssetRepoInterface)(nil).StoreAssetImage), arg0, arg1) } +// UpdateContractId mocks base method. +func (m *MockAssetRepoInterface) UpdateContractId(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContractId", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateContractId indicates an expected call of UpdateContractId. +func (mr *MockAssetRepoInterfaceMockRecorder) UpdateContractId(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContractId", reflect.TypeOf((*MockAssetRepoInterface)(nil).UpdateContractId), arg0, arg1) +} + // MockRoleRepoInterface is a mock of RoleRepoInterface interface. type MockRoleRepoInterface struct { ctrl *gomock.Controller @@ -944,12 +959,13 @@ func (mr *MockVaultRepoInterfaceMockRecorder) DeleteVault(arg0 interface{}) *gom } // GetPaginatedVaults mocks base method. -func (m *MockVaultRepoInterface) GetPaginatedVaults(arg0, arg1 int) ([]entity.Vault, error) { +func (m *MockVaultRepoInterface) GetPaginatedVaults(arg0, arg1 int) ([]entity.Vault, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPaginatedVaults", arg0, arg1) ret0, _ := ret[0].([]entity.Vault) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetPaginatedVaults indicates an expected call of GetPaginatedVaults. @@ -974,18 +990,18 @@ func (mr *MockVaultRepoInterfaceMockRecorder) GetVaultById(id interface{}) *gomo } // GetVaults mocks base method. -func (m *MockVaultRepoInterface) GetVaults() ([]entity.Vault, error) { +func (m *MockVaultRepoInterface) GetVaults(isAll bool) ([]entity.Vault, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVaults") + ret := m.ctrl.Call(m, "GetVaults", isAll) ret0, _ := ret[0].([]entity.Vault) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVaults indicates an expected call of GetVaults. -func (mr *MockVaultRepoInterfaceMockRecorder) GetVaults() *gomock.Call { +func (mr *MockVaultRepoInterfaceMockRecorder) GetVaults(isAll interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVaults", reflect.TypeOf((*MockVaultRepoInterface)(nil).GetVaults)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVaults", reflect.TypeOf((*MockVaultRepoInterface)(nil).GetVaults), isAll) } // UpdateVault mocks base method. @@ -1026,6 +1042,21 @@ func (m *MockContractRepoInterface) EXPECT() *MockContractRepoInterfaceMockRecor return m.recorder } +// AddContractHistory mocks base method. +func (m *MockContractRepoInterface) AddContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddContractHistory", contractHistory) + ret0, _ := ret[0].(entity.ContractHistory) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddContractHistory indicates an expected call of AddContractHistory. +func (mr *MockContractRepoInterfaceMockRecorder) AddContractHistory(contractHistory interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddContractHistory", reflect.TypeOf((*MockContractRepoInterface)(nil).AddContractHistory), contractHistory) +} + // CreateContract mocks base method. func (m *MockContractRepoInterface) CreateContract(arg0 entity.Contract) (entity.Contract, error) { m.ctrl.T.Helper() @@ -1071,13 +1102,29 @@ func (mr *MockContractRepoInterfaceMockRecorder) GetContracts() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContracts", reflect.TypeOf((*MockContractRepoInterface)(nil).GetContracts)) } +// GetHistory mocks base method. +func (m *MockContractRepoInterface) GetHistory(userId, contractId int) ([]entity.ContractHistory, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHistory", userId, contractId) + ret0, _ := ret[0].([]entity.ContractHistory) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHistory indicates an expected call of GetHistory. +func (mr *MockContractRepoInterfaceMockRecorder) GetHistory(userId, contractId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHistory", reflect.TypeOf((*MockContractRepoInterface)(nil).GetHistory), userId, contractId) +} + // GetPaginatedContracts mocks base method. -func (m *MockContractRepoInterface) GetPaginatedContracts(arg0, arg1 int) ([]entity.Contract, error) { +func (m *MockContractRepoInterface) GetPaginatedContracts(arg0, arg1 int) ([]entity.Contract, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPaginatedContracts", arg0, arg1) ret0, _ := ret[0].([]entity.Contract) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetPaginatedContracts indicates an expected call of GetPaginatedContracts. @@ -1086,6 +1133,21 @@ func (mr *MockContractRepoInterfaceMockRecorder) GetPaginatedContracts(arg0, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPaginatedContracts", reflect.TypeOf((*MockContractRepoInterface)(nil).GetPaginatedContracts), arg0, arg1) } +// UpdateContractHistory mocks base method. +func (m *MockContractRepoInterface) UpdateContractHistory(contractHistory entity.ContractHistory) (entity.ContractHistory, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContractHistory", contractHistory) + ret0, _ := ret[0].(entity.ContractHistory) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateContractHistory indicates an expected call of UpdateContractHistory. +func (mr *MockContractRepoInterfaceMockRecorder) UpdateContractHistory(contractHistory interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContractHistory", reflect.TypeOf((*MockContractRepoInterface)(nil).UpdateContractHistory), contractHistory) +} + // MockLogTransactionRepoInterface is a mock of LogTransactionRepoInterface interface. type MockLogTransactionRepoInterface struct { ctrl *gomock.Controller @@ -1257,3 +1319,41 @@ func (mr *MockLogTransactionRepoInterfaceMockRecorder) SumLogTransactionsByAsset mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SumLogTransactionsByAssetID", reflect.TypeOf((*MockLogTransactionRepoInterface)(nil).SumLogTransactionsByAssetID), assetID, timeRange, timeFrame, transactionType) } + +// MockAssetServiceInterface is a mock of AssetServiceInterface interface. +type MockAssetServiceInterface struct { + ctrl *gomock.Controller + recorder *MockAssetServiceInterfaceMockRecorder +} + +// MockAssetServiceInterfaceMockRecorder is the mock recorder for MockAssetServiceInterface. +type MockAssetServiceInterfaceMockRecorder struct { + mock *MockAssetServiceInterface +} + +// NewMockAssetServiceInterface creates a new mock instance. +func NewMockAssetServiceInterface(ctrl *gomock.Controller) *MockAssetServiceInterface { + mock := &MockAssetServiceInterface{ctrl: ctrl} + mock.recorder = &MockAssetServiceInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAssetServiceInterface) EXPECT() *MockAssetServiceInterfaceMockRecorder { + return m.recorder +} + +// UploadFile mocks base method. +func (m *MockAssetServiceInterface) UploadFile(arg0 string, arg1 []byte) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UploadFile", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UploadFile indicates an expected call of UploadFile. +func (mr *MockAssetServiceInterfaceMockRecorder) UploadFile(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadFile", reflect.TypeOf((*MockAssetServiceInterface)(nil).UploadFile), arg0, arg1) +} diff --git a/backend/internal/usecase/repo/asset_postgres.go b/backend/internal/usecase/repo/asset_postgres.go index c579ceda..c1172eee 100644 --- a/backend/internal/usecase/repo/asset_postgres.go +++ b/backend/internal/usecase/repo/asset_postgres.go @@ -36,23 +36,24 @@ func (r AssetRepo) GetAsset(id int) (entity.Asset, error) { func (r AssetRepo) GetAssets() ([]entity.Asset, error) { query := ` - SELECT - a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, a.image, - d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, - dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, - i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, - ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight - FROM asset a - JOIN wallet d ON a.distributor_id = d.id - JOIN key dk ON d.id = dk.wallet_id - JOIN wallet i ON a.issuer_id = i.id - JOIN key ik ON i.id = ik.wallet_id - ORDER BY a.name; - ` + SELECT + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, + COALESCE(a.image, '') AS image, a.contract_id, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight + FROM asset a + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id + ORDER BY a.name; + ` rows, err := r.Db.Query(query) if err != nil { - return nil, fmt.Errorf("AssetRepo - GetAllAssets - Query: %w", err) + return nil, fmt.Errorf("AssetRepo - GetAssets - Query: %w", err) } defer rows.Close() @@ -62,16 +63,24 @@ func (r AssetRepo) GetAssets() ([]entity.Asset, error) { var asset entity.Asset var distributor entity.Wallet var issuer entity.Wallet + var image sql.NullString err := rows.Scan( - &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &asset.Image, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &asset.ContractId, &distributor.Id, &distributor.Type, &distributor.Funded, &distributor.Key.Id, &distributor.Key.PublicKey, &distributor.Key.Weight, &issuer.Id, &issuer.Type, &issuer.Funded, &issuer.Key.Id, &issuer.Key.PublicKey, &issuer.Key.Weight, ) if err != nil { - return nil, fmt.Errorf("AssetRepo - GetAllAssets - row.Scan: %w", err) + return nil, fmt.Errorf("AssetRepo - GetAssets - row.Scan: %w", err) + } + + if image.Valid { + asset.Image = image.String + } else { + asset.Image = "" } asset.Distributor = distributor @@ -85,28 +94,31 @@ func (r AssetRepo) GetAssets() ([]entity.Asset, error) { func (r AssetRepo) GetAssetByCode(code string) (entity.Asset, error) { query := ` - SELECT - a.id AS asset_id, a.name AS asset_name, a.asset_type,a.code as asset_code, a.image, - d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, - dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, - i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, - ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight - FROM asset a - JOIN wallet d ON a.distributor_id = d.id - JOIN key dk ON d.id = dk.wallet_id - JOIN wallet i ON a.issuer_id = i.id - JOIN key ik ON i.id = ik.wallet_id - WHERE a.code = $1; - ` + SELECT + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, + COALESCE(a.image, '') AS image, a.contract_id, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight + FROM asset a + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id + WHERE a.code = $1; + ` row := r.Db.QueryRow(query, code) var asset entity.Asset var distributor entity.Wallet var issuer entity.Wallet + var image sql.NullString err := row.Scan( - &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &asset.Image, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &asset.ContractId, &distributor.Id, &distributor.Type, &distributor.Funded, &distributor.Key.Id, &distributor.Key.PublicKey, &distributor.Key.Weight, &issuer.Id, &issuer.Type, &issuer.Funded, @@ -119,47 +131,45 @@ func (r AssetRepo) GetAssetByCode(code string) (entity.Asset, error) { return entity.Asset{}, fmt.Errorf("AssetRepo - GetAssetByCode - row.Scan: %w", err) } + if image.Valid { + asset.Image = image.String + } else { + asset.Image = "" + } + asset.Distributor = distributor asset.Issuer = issuer return asset, nil } -func (r AssetRepo) CreateAsset(data entity.Asset) (entity.Asset, error) { - res := data - stmt := `INSERT INTO Asset (code, issuer_id, distributor_id, name, asset_type, image) VALUES ($1, $2, $3,$4, $5, $6) RETURNING id;` - err := r.Db.QueryRow(stmt, data.Code, data.Issuer.Id, data.Distributor.Id, data.Name, data.AssetType, data.Image).Scan(&res.Id) - if err != nil { - return entity.Asset{}, fmt.Errorf("AssetRepo - CreateAsset - db.QueryRow: %w", err) - } - - return res, nil -} - func (r AssetRepo) GetAssetById(id string) (entity.Asset, error) { query := ` - SELECT - a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code as asset_code, a.image, - d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, - dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, - i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, - ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight - FROM asset a - JOIN wallet d ON a.distributor_id = d.id - JOIN key dk ON d.id = dk.wallet_id - JOIN wallet i ON a.issuer_id = i.id - JOIN key ik ON i.id = ik.wallet_id - WHERE a.id = $1; - ` + SELECT + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, + COALESCE(a.image, '') AS image, a.contract_id, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight + FROM asset a + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id + WHERE a.id = $1; + ` row := r.Db.QueryRow(query, id) var asset entity.Asset var distributor entity.Wallet var issuer entity.Wallet + var image sql.NullString err := row.Scan( - &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &asset.Image, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &asset.ContractId, &distributor.Id, &distributor.Type, &distributor.Funded, &distributor.Key.Id, &distributor.Key.PublicKey, &distributor.Key.Weight, &issuer.Id, &issuer.Type, &issuer.Funded, @@ -172,68 +182,58 @@ func (r AssetRepo) GetAssetById(id string) (entity.Asset, error) { return entity.Asset{}, fmt.Errorf("AssetRepo - GetAssetById - row.Scan: %w", err) } + if image.Valid { + asset.Image = image.String + } else { + asset.Image = "" + } + asset.Distributor = distributor asset.Issuer = issuer return asset, nil } -func (r AssetRepo) StoreAssetImage(assetId string, imageBytes []byte) error { - stmt := ` - UPDATE asset - SET image = $2 - WHERE id = $1 - ` - - _, err := r.Db.Exec(stmt, assetId, imageBytes) - if err != nil { - return fmt.Errorf("AssetRepo - StoreAssetImage - Db.Exec: %w", err) - } - - return nil -} - -func (r AssetRepo) GetAssetImage(assetId string) ([]byte, error) { - stmt := ` - SELECT image - FROM asset - WHERE id = $1 - ` - - row := r.Db.QueryRow(stmt, assetId) +func (r AssetRepo) GetPaginatedAssets(page int, limit int) ([]entity.Asset, int, error) { + // Calculate the offset + offset := (page - 1) * limit - var image []byte - err := row.Scan(&image) + // Query to count the total number of assets + countQuery := ` + SELECT COUNT(*) + FROM asset; + ` + var totalAssets int + err := r.Db.QueryRow(countQuery).Scan(&totalAssets) if err != nil { - return nil, fmt.Errorf("AssetRepo - GetAssetImage - row.Scan: %w", err) + return nil, 0, fmt.Errorf("AssetRepo - GetPaginated - Count Query: %w", err) } - return image, nil -} - -func (r AssetRepo) GetPaginatedAssets(page int, limit int) ([]entity.Asset, error) { - // Calculate offset - offset := (page - 1) * limit + // Calculate total pages + totalPages := (totalAssets + limit - 1) / limit + // Query to fetch paginated assets query := ` - SELECT - a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, a.image, - d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, - dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, - i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, - ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight - FROM asset a - JOIN wallet d ON a.distributor_id = d.id - JOIN key dk ON d.id = dk.wallet_id - JOIN wallet i ON a.issuer_id = i.id - JOIN key ik ON i.id = ik.wallet_id - ORDER BY a.name - LIMIT $1 OFFSET $2; + SELECT + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code AS code, + COALESCE(a.image, '') AS image, a.contract_id, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight + FROM asset a + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id + ORDER BY a.name + LIMIT $1 OFFSET $2; + ` rows, err := r.Db.Query(query, limit, offset) if err != nil { - return nil, fmt.Errorf("AssetRepo - GetPaginated - Query: %w", err) + return nil, 0, fmt.Errorf("AssetRepo - GetPaginated - Query: %w", err) } defer rows.Close() @@ -242,23 +242,95 @@ func (r AssetRepo) GetPaginatedAssets(page int, limit int) ([]entity.Asset, erro var asset entity.Asset var distributor entity.Wallet var issuer entity.Wallet + var image sql.NullString err := rows.Scan( - &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &asset.Image, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &asset.ContractId, &distributor.Id, &distributor.Type, &distributor.Funded, &distributor.Key.Id, &distributor.Key.PublicKey, &distributor.Key.Weight, &issuer.Id, &issuer.Type, &issuer.Funded, &issuer.Key.Id, &issuer.Key.PublicKey, &issuer.Key.Weight, ) if err != nil { - return nil, fmt.Errorf("AssetRepo - GetPaginated - row.Scan: %w", err) + return nil, 0, fmt.Errorf("AssetRepo - GetPaginated - row.Scan: %w", err) } + if !image.Valid { + asset.Image = "" + } else { + asset.Image = image.String + } asset.Distributor = distributor asset.Issuer = issuer assets = append(assets, asset) } + return assets, totalPages, nil +} - return assets, nil +func (r AssetRepo) StoreAssetImage(assetId string, image string) error { + stmt := ` + UPDATE asset + SET image = $2 + WHERE id = $1 + ` + + _, err := r.Db.Exec(stmt, assetId, image) + if err != nil { + return fmt.Errorf("AssetRepo - StoreAssetImage - Db.Exec: %w", err) + } + + return nil +} + +func (r AssetRepo) GetAssetImage(assetId string) ([]byte, error) { + stmt := ` + SELECT image + FROM asset + WHERE id = $1 + ` + + row := r.Db.QueryRow(stmt, assetId) + + var image sql.NullString + err := row.Scan(&image) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("AssetRepo - GetAssetImage - asset not found: %w", err) + } + return nil, fmt.Errorf("AssetRepo - GetAssetImage - row.Scan: %w", err) + } + + if image.Valid { + return []byte(image.String), nil + } + + return nil, nil // No image found, return nil without error +} + +func (r AssetRepo) UpdateContractId(assetId string, contractId string) error { + stmt := ` + UPDATE asset + SET contract_id = $2 + WHERE id = $1 + ` + + _, err := r.Db.Exec(stmt, assetId, contractId) + if err != nil { + return fmt.Errorf("AssetRepo - UpdateContractId - Db.Exec: %w", err) + } + + return nil +} + +func (r AssetRepo) CreateAsset(data entity.Asset) (entity.Asset, error) { + res := data + stmt := `INSERT INTO Asset (code, issuer_id, distributor_id, name, asset_type, image ) VALUES ($1, $2, $3,$4, $5, $6) RETURNING id;` + err := r.Db.QueryRow(stmt, data.Code, data.Issuer.Id, data.Distributor.Id, data.Name, data.AssetType, data.Image).Scan(&res.Id) + if err != nil { + return entity.Asset{}, fmt.Errorf("AssetRepo - CreateAsset - db.QueryRow: %w", err) + } + + return res, nil } diff --git a/backend/internal/usecase/repo/contract_postgres.go b/backend/internal/usecase/repo/contract_postgres.go index 6b478c5a..24371c0a 100644 --- a/backend/internal/usecase/repo/contract_postgres.go +++ b/backend/internal/usecase/repo/contract_postgres.go @@ -21,15 +21,26 @@ func (r ContractRepo) GetContracts() ([]entity.Contract, error) { SELECT c.id AS contract_id, c.name AS contract_name, c.address AS contract_address, c.yield_rate AS contract_yield_rate, c.term AS contract_term, c.min_deposit AS contract_min_deposit, c.penalty_rate AS contract_penalty_rate, - v.id AS vault_id, v.name AS vault_name, + c.compound AS contract_compound, c.created_at AS contract_created_at, v.id AS vault_id, v.name AS vault_name, vc.id AS vault_category_id, vc.name as vault_category_name, w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, - wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight + wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight, + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code as asset_code, COALESCE(a.image, '') AS image, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight FROM contracts c JOIN vault v ON c.vault_id = v.id JOIN vaultcategory vc ON v.vault_category_id = vc.id JOIN wallet w ON v.wallet_id = w.id - JOIN key wk ON w.id = wk.wallet_id; + JOIN key wk ON w.id = wk.wallet_id + JOIN asset a ON a.id = c.asset_id + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id + ORDER BY c.id DESC ` rows, err := r.Db.Query(query) @@ -46,20 +57,36 @@ func (r ContractRepo) GetContracts() ([]entity.Contract, error) { var vault entity.Vault var asset entity.Asset var wallet entity.Wallet + var assetDistributor entity.Wallet + var assetIssuer entity.Wallet + var image sql.NullString err := rows.Scan( - &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, &contract.Term, &contract.MinDeposit, &contract.PenaltyRate, + &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, &contract.Term, &contract.MinDeposit, + &contract.PenaltyRate, &contract.Compound, &contract.CreatedAt, &vault.Id, &vault.Name, &vaultCategory.Id, &vaultCategory.Name, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &assetDistributor.Id, &assetDistributor.Type, &assetDistributor.Funded, + &assetDistributor.Key.Id, &assetDistributor.Key.PublicKey, &assetDistributor.Key.Weight, + &assetIssuer.Id, &assetIssuer.Type, &assetIssuer.Funded, + &assetIssuer.Key.Id, &assetIssuer.Key.PublicKey, &assetIssuer.Key.Weight, ) if err != nil { return nil, fmt.Errorf("ContractRepo - GetContractCategories - row.Scan: %w", err) } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + vault.VaultCategory = &vaultCategory + asset.Distributor = assetDistributor + asset.Issuer = assetIssuer + if !image.Valid { + asset.Image = "" + } else { + asset.Image = image.String + } contract.Asset = asset contract.Vault = vault @@ -71,18 +98,28 @@ func (r ContractRepo) GetContracts() ([]entity.Contract, error) { func (r ContractRepo) GetContractById(id string) (entity.Contract, error) { query := ` - SELECT - c.id AS contract_id, c.name AS contract_name, c.address AS contract_address, c.yield_rate AS contract_yield_rate, - c.term AS contract_term, c.min_deposit AS contract_min_deposit, c.penalty_rate AS contract_penalty_rate, - v.id AS vault_id, v.name AS vault_name, - vc.id AS vault_category_id, vc.name as vault_category_name, - w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, - wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight + SELECT + c.id AS contract_id, c.name AS contract_name, c.address AS contract_address, c.yield_rate AS contract_yield_rate, + c.term AS contract_term, c.min_deposit AS contract_min_deposit, c.penalty_rate AS contract_penalty_rate, + c.compound AS contract_compound, c.created_at AS contract_created_at, v.id AS vault_id, v.name AS vault_name, + vc.id AS vault_category_id, vc.name as vault_category_name, + w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, + wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight, + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code as asset_code, COALESCE(a.image, '') AS image, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight FROM contracts c JOIN vault v ON c.vault_id = v.id JOIN vaultcategory vc ON v.vault_category_id = vc.id JOIN wallet w ON v.wallet_id = w.id JOIN key wk ON w.id = wk.wallet_id + JOIN asset a ON a.id = c.asset_id + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id WHERE c.id = $1; ` @@ -93,13 +130,22 @@ func (r ContractRepo) GetContractById(id string) (entity.Contract, error) { var vault entity.Vault var asset entity.Asset var wallet entity.Wallet + var assetDistributor entity.Wallet + var assetIssuer entity.Wallet + var image sql.NullString err := row.Scan( - &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, &contract.Term, &contract.MinDeposit, &contract.PenaltyRate, + &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, &contract.Term, &contract.MinDeposit, + &contract.PenaltyRate, &contract.Compound, &contract.CreatedAt, &vault.Id, &vault.Name, &vaultCategory.Id, &vaultCategory.Name, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &assetDistributor.Id, &assetDistributor.Type, &assetDistributor.Funded, + &assetDistributor.Key.Id, &assetDistributor.Key.PublicKey, &assetDistributor.Key.Weight, + &assetIssuer.Id, &assetIssuer.Type, &assetIssuer.Funded, + &assetIssuer.Key.Id, &assetIssuer.Key.PublicKey, &assetIssuer.Key.Weight, ) if err != nil { if err == sql.ErrNoRows { @@ -109,7 +155,14 @@ func (r ContractRepo) GetContractById(id string) (entity.Contract, error) { } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + vault.VaultCategory = &vaultCategory + asset.Distributor = assetDistributor + asset.Issuer = assetIssuer + if !image.Valid { + asset.Image = "" + } else { + asset.Image = image.String + } contract.Asset = asset contract.Vault = vault @@ -118,8 +171,12 @@ func (r ContractRepo) GetContractById(id string) (entity.Contract, error) { func (r ContractRepo) CreateContract(data entity.Contract) (entity.Contract, error) { res := data - stmt := `INSERT INTO Contract (name, address, yield_rate, term, min_deposit, penalty_rate, vault_id, asset_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id;` - err := r.Db.QueryRow(stmt, data.Name, data.Address, data.YieldRate, data.Term, data.MinDeposit, data.PenaltyRate, data.Vault.Id, data.Asset.Id).Scan(&res.Id) + stmt := `INSERT INTO Contracts (name, address, yield_rate, term, min_deposit, penalty_rate, compound, vault_id, asset_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id;` + + err := r.Db.QueryRow(stmt, data.Name, data.Address, data.YieldRate, data.Term, data.MinDeposit, + data.PenaltyRate, data.Compound, data.Vault.Id, data.Asset.Id).Scan(&res.Id) + if err != nil { return entity.Contract{}, fmt.Errorf("ContractRepo - Contract - db.QueryRow: %w", err) } @@ -127,29 +184,51 @@ func (r ContractRepo) CreateContract(data entity.Contract) (entity.Contract, err return res, nil } -func (r ContractRepo) GetPaginatedContracts(page, limit int) ([]entity.Contract, error) { +func (r ContractRepo) GetPaginatedContracts(page, limit int) ([]entity.Contract, int, error) { offset := (page - 1) * limit + + // Query to count the total number of vaults + countQuery := `SELECT COUNT(*) FROM contracts;` + + var totalVaults int + err := r.Db.QueryRow(countQuery).Scan(&totalVaults) + if err != nil { + return nil, 0, fmt.Errorf("ContractRepo - GetPaginatedContracts - Count Query: %w", err) + } + + // Calculate total pages + totalPages := (totalVaults + limit - 1) / limit + query := ` - SELECT - c.id AS contract_id, c.name AS contract_name, c.address AS contract_address, - c.yield_rate AS contract_yield_rate, c.term AS contract_term, - c.min_deposit AS contract_min_deposit, c.penalty_rate AS contract_penalty_rate, - v.id AS vault_id, v.name AS vault_name, + SELECT + c.id AS contract_id, c.name AS contract_name, c.address AS contract_address, c.yield_rate AS contract_yield_rate, + c.term AS contract_term, c.min_deposit AS contract_min_deposit, c.penalty_rate AS contract_penalty_rate, + c.compound AS contract_compound, c.created_at AS contract_created_at, v.id AS vault_id, v.name AS vault_name, vc.id AS vault_category_id, vc.name as vault_category_name, w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, - wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight + wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight, + a.id AS asset_id, a.name AS asset_name, a.asset_type, a.code as asset_code, COALESCE(a.image, '') AS image, + d.id AS distributor_id, d.type AS distributor_type, d.funded AS distributor_funded, + dk.id AS distributor_key_id, dk.public_key AS distributor_key_public_key, dk.weight AS distributor_key_weight, + i.id AS issuer_id, i.type AS issuer_type, i.funded AS issuer_funded, + ik.id AS issuer_key_id, ik.public_key AS issuer_key_public_key, ik.weight AS issuer_key_weight FROM contracts c JOIN vault v ON c.vault_id = v.id JOIN vaultcategory vc ON v.vault_category_id = vc.id JOIN wallet w ON v.wallet_id = w.id JOIN key wk ON w.id = wk.wallet_id + JOIN asset a ON a.id = c.asset_id + JOIN wallet d ON a.distributor_id = d.id + JOIN key dk ON d.id = dk.wallet_id + JOIN wallet i ON a.issuer_id = i.id + JOIN key ik ON i.id = ik.wallet_id ORDER BY c.id DESC LIMIT $1 OFFSET $2; ` rows, err := r.Db.Query(query, limit, offset) if err != nil { - return nil, fmt.Errorf("ContractRepo - GetPaginatedContracts - Query: %w", err) + return nil, 0, fmt.Errorf("ContractRepo - GetPaginatedContracts - Query: %w", err) } defer rows.Close() @@ -159,26 +238,108 @@ func (r ContractRepo) GetPaginatedContracts(page, limit int) ([]entity.Contract, var contract entity.Contract var vaultCategory entity.VaultCategory var vault entity.Vault + var asset entity.Asset var wallet entity.Wallet + var assetDistributor entity.Wallet + var assetIssuer entity.Wallet + var image sql.NullString err := rows.Scan( - &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, - &contract.Term, &contract.MinDeposit, &contract.PenaltyRate, + &contract.Id, &contract.Name, &contract.Address, &contract.YieldRate, &contract.Term, &contract.MinDeposit, + &contract.PenaltyRate, &contract.Compound, &contract.CreatedAt, &vault.Id, &vault.Name, &vaultCategory.Id, &vaultCategory.Name, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, + &asset.Id, &asset.Name, &asset.AssetType, &asset.Code, &image, + &assetDistributor.Id, &assetDistributor.Type, &assetDistributor.Funded, + &assetDistributor.Key.Id, &assetDistributor.Key.PublicKey, &assetDistributor.Key.Weight, + &assetIssuer.Id, &assetIssuer.Type, &assetIssuer.Funded, + &assetIssuer.Key.Id, &assetIssuer.Key.PublicKey, &assetIssuer.Key.Weight, ) if err != nil { - return nil, fmt.Errorf("ContractRepo - GetPaginatedContracts - row.Scan: %w", err) + return nil, 0, fmt.Errorf("ContractRepo - GetPaginatedContracts - row.Scan: %w", err) } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + vault.VaultCategory = &vaultCategory + asset.Distributor = assetDistributor + asset.Issuer = assetIssuer + if !image.Valid { + asset.Image = "" + } else { + asset.Image = image.String + } + contract.Asset = asset contract.Vault = vault contracts = append(contracts, contract) } - return contracts, nil + return contracts, totalPages, nil +} + +func (r ContractRepo) GetHistory(userId int, contractId int) ([]entity.ContractHistory, error) { + query := ` + SELECT ch.id, ch.deposited_at, ch.deposit_amount, ch.withdrawn_at, ch.withdraw_amount, ch.deposited_at + FROM contractshistory ch + WHERE ch.user_id = $1 AND ch.contract_id = $2 + ORDER BY ch.deposited_at DESC; + ` + + rows, err := r.Db.Query(query, userId, contractId) + if err != nil { + return nil, fmt.Errorf("ContractRepo - GetHistory - Query: %w", err) + } + defer rows.Close() + + contractsHistory := []entity.ContractHistory{} + + for rows.Next() { + var contractHistory entity.ContractHistory + + err := rows.Scan( + &contractHistory.Id, &contractHistory.DepositedAt, &contractHistory.DepositAmount, &contractHistory.WithdrawnAt, + &contractHistory.WithdrawAmount, &contractHistory.DepositedAt, + ) + if err != nil { + return nil, fmt.Errorf("ContractRepo - GetContractCategories - row.Scan: %w", err) + } + + contractsHistory = append(contractsHistory, contractHistory) + } + + return contractsHistory, nil +} + +func (r ContractRepo) AddContractHistory(data entity.ContractHistory) (entity.ContractHistory, error) { + res := data + stmt := `INSERT INTO contractshistory (deposit_amount, contract_id, user_id) + VALUES ($1, $2, $3) RETURNING id;` + + err := r.Db.QueryRow(stmt, data.DepositAmount, data.Contract.Id, data.User.ID).Scan(&res.Id) + + if err != nil { + return entity.ContractHistory{}, fmt.Errorf("ContractRepo - addContractHistory - db.QueryRow: %w", err) + } + + return res, nil +} + +func (r ContractRepo) UpdateContractHistory(data entity.ContractHistory) (entity.ContractHistory, error) { + res := data + stmt := `UPDATE ContractsHistory SET withdraw_amount = $1, withdrawn_at = now() WHERE id = ( + SELECT id + FROM ContractsHistory + WHERE withdraw_amount IS NULL AND contract_id = $2 AND user_id = $3 + ORDER BY id DESC + LIMIT 1) RETURNING id;` + + err := r.Db.QueryRow(stmt, data.WithdrawAmount, data.Contract.Id, data.User.ID).Scan(&res.Id) + + if err != nil { + return entity.ContractHistory{}, fmt.Errorf("ContractRepo - updateContractHistory - db.QueryRow: %w", err) + } + + return res, nil } diff --git a/backend/internal/usecase/repo/user_postgres.go b/backend/internal/usecase/repo/user_postgres.go index e4a3797d..89ca9642 100644 --- a/backend/internal/usecase/repo/user_postgres.go +++ b/backend/internal/usecase/repo/user_postgres.go @@ -126,13 +126,14 @@ func (r UserRepo) EditUsersRole(id_user string, role_id string) error { } func (r UserRepo) GetProfile(token string) (entity.UserResponse, error) { - stmt := `SELECT u.id, u.name, u.updated_at, u.role_id, r.name as role, u.email + stmt := `SELECT u.id, u.name, u.updated_at, u.role_id, r.name as role, u.email, v.id as vault_id FROM UserAccount u LEFT JOIN Role r ON u.role_id = r.id + LEFT JOIN Vault v ON u.id = v.owner_id WHERE u.token = $1` var user entity.UserResponse - err := r.Db.QueryRow(stmt, token).Scan(&user.ID, &user.Name, &user.UpdatedAt, &user.RoleId, &user.Role, &user.Email) + err := r.Db.QueryRow(stmt, token).Scan(&user.ID, &user.Name, &user.UpdatedAt, &user.RoleId, &user.Role, &user.Email, &user.VaultId) if err != nil { return entity.UserResponse{}, fmt.Errorf("UserRepo - GetProfile - db.Query: %w", err) } diff --git a/backend/internal/usecase/repo/vault_postgres.go b/backend/internal/usecase/repo/vault_postgres.go index 60dac20c..127e40c9 100644 --- a/backend/internal/usecase/repo/vault_postgres.go +++ b/backend/internal/usecase/repo/vault_postgres.go @@ -16,20 +16,28 @@ func NewVaultRepo(pg *postgres.Postgres) VaultRepo { return VaultRepo{pg} } -func (r VaultRepo) GetVaults() ([]entity.Vault, error) { - query := ` +func (r VaultRepo) GetVaults(isAll bool) ([]entity.Vault, error) { + baseQuery := ` SELECT - v.id AS vault_id, v.name AS vault_name, v.active AS vault_active, + v.id AS vault_id, v.name AS vault_name, v.active AS vault_active, v.owner_id AS owner_id, vc.id AS vault_category_id, vc.name as vault_category_name, vc.theme as vault_category_theme, w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight FROM vault v - JOIN vaultcategory vc ON v.vault_category_id = vc.id + LEFT JOIN vaultcategory vc ON v.vault_category_id = vc.id JOIN wallet w ON v.wallet_id = w.id JOIN key wk ON w.id = wk.wallet_id - WHERE v.active = 1; ` + whereClause := ` WHERE v.active = 1` + orderClause := ` ORDER BY v.name ASC` + + if !isAll { + whereClause = whereClause + ` AND v.owner_id is null` + } + + query := baseQuery + whereClause + orderClause + rows, err := r.Db.Query(query) if err != nil { return nil, fmt.Errorf("VaultRepo - GetVaultCategories - Query: %w", err) @@ -40,12 +48,14 @@ func (r VaultRepo) GetVaults() ([]entity.Vault, error) { for rows.Next() { var vault entity.Vault - var vaultCategory entity.VaultCategory + var vaultCategoryId sql.NullInt64 + var vaultCategoryName sql.NullString + var vaultCategoryTheme sql.NullString var wallet entity.Wallet err := rows.Scan( - &vault.Id, &vault.Name, &vault.Active, - &vaultCategory.Id, &vaultCategory.Name, &vaultCategory.Theme, + &vault.Id, &vault.Name, &vault.Active, &vault.OwnerId, + &vaultCategoryId, &vaultCategoryName, &vaultCategoryTheme, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, ) @@ -54,7 +64,13 @@ func (r VaultRepo) GetVaults() ([]entity.Vault, error) { } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + if vaultCategoryId.Valid { + vault.VaultCategory = &entity.VaultCategory{ + Id: int(vaultCategoryId.Int64), + Name: vaultCategoryName.String, + Theme: &vaultCategoryTheme.String, + } + } vaults = append(vaults, vault) } @@ -65,12 +81,12 @@ func (r VaultRepo) GetVaults() ([]entity.Vault, error) { func (r VaultRepo) GetVaultById(id int) (entity.Vault, error) { query := ` SELECT - v.id AS vault_id, v.name AS vault_name, v.active as vault_active, + v.id AS vault_id, v.name AS vault_name, v.active as vault_active, v.owner_id AS owner_id, vc.id AS vault_category_id, vc.name as vault_category_name, vc.theme as vault_category_theme, w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight FROM vault v - JOIN vaultcategory vc ON v.vault_category_id = vc.id + LEFT JOIN vaultcategory vc ON v.vault_category_id = vc.id JOIN wallet w ON v.wallet_id = w.id JOIN key wk ON w.id = wk.wallet_id WHERE v.id = $1; @@ -79,12 +95,14 @@ func (r VaultRepo) GetVaultById(id int) (entity.Vault, error) { row := r.Db.QueryRow(query, id) var vault entity.Vault - var vaultCategory entity.VaultCategory + var vaultCategoryId sql.NullInt64 + var vaultCategoryName sql.NullString + var vaultCategoryTheme sql.NullString var wallet entity.Wallet err := row.Scan( - &vault.Id, &vault.Name, &vault.Active, - &vaultCategory.Id, &vaultCategory.Name, &vaultCategory.Theme, + &vault.Id, &vault.Name, &vault.Active, &vault.OwnerId, + &vaultCategoryId, &vaultCategoryName, &vaultCategoryTheme, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, ) @@ -96,15 +114,34 @@ func (r VaultRepo) GetVaultById(id int) (entity.Vault, error) { } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + if vaultCategoryId.Valid { + vault.VaultCategory = &entity.VaultCategory{ + Id: int(vaultCategoryId.Int64), + Name: vaultCategoryName.String, + Theme: &vaultCategoryTheme.String, + } + } return vault, nil } func (r VaultRepo) CreateVault(data entity.Vault) (entity.Vault, error) { res := data - stmt := `INSERT INTO Vault (name, vault_category_id, wallet_id) VALUES ($1, $2, $3) RETURNING id;` - err := r.Db.QueryRow(stmt, data.Name, data.VaultCategory.Id, data.Wallet.Id).Scan(&res.Id) + var vaultCategoryId *int + + if data.VaultCategory.Id != 0 { + vaultCategoryId = &data.VaultCategory.Id + } + + var stmt string + if data.OwnerId != nil { + stmt = `INSERT INTO Vault (name, vault_category_id, wallet_id, owner_id) + VALUES (CONCAT(CAST($1 AS TEXT), '#', NEXTVAL('vault_id_seq')), $2, $3, $4) RETURNING id;` + } else { + stmt = `INSERT INTO Vault (name, vault_category_id, wallet_id, owner_id) + VALUES ($1, $2, $3, $4) RETURNING id;` + } + err := r.Db.QueryRow(stmt, data.Name, vaultCategoryId, data.Wallet.Id, data.OwnerId).Scan(&res.Id) if err != nil { return entity.Vault{}, fmt.Errorf("VaultRepo - Vault - db.QueryRow: %w", err) } @@ -139,13 +176,26 @@ func (r VaultRepo) DeleteVault(data entity.Vault) (entity.Vault, error) { } // GetPaginatedVaults -. -func (r VaultRepo) GetPaginatedVaults(page, limit int) ([]entity.Vault, error) { +func (r VaultRepo) GetPaginatedVaults(page, limit int) ([]entity.Vault, int, error) { // Calculate offset based on page number and limit offset := (page - 1) * limit + // Query to count the total number of vaults + countQuery := `SELECT COUNT(*) FROM vault v WHERE v.active = 1 AND v.owner_id is null;` + + var totalVaults int + err := r.Db.QueryRow(countQuery).Scan(&totalVaults) + if err != nil { + return nil, 0, fmt.Errorf("VaultRepo - GetPaginatedVaults - Count Query: %w", err) + } + + // Calculate total pages + totalPages := (totalVaults + limit - 1) / limit + + // Query to fetch paginated assets query := ` SELECT - v.id AS vault_id, v.name AS vault_name, v.active AS vault_active, + v.id AS vault_id, v.name AS vault_name, v.active AS vault_active, v.owner_id AS owner_id, vc.id AS vault_category_id, vc.name as vault_category_name, vc.theme as vault_category_theme, w.id AS wallet_id, w.type AS wallet_type, w.funded AS wallet_funded, wk.id AS wallet_key_id, wk.public_key AS wallet_key_public_key, wk.weight AS wallet_key_weight @@ -154,13 +204,13 @@ func (r VaultRepo) GetPaginatedVaults(page, limit int) ([]entity.Vault, error) { JOIN wallet w ON v.wallet_id = w.id JOIN key wk ON w.id = wk.wallet_id WHERE v.active = 1 - ORDER BY v.id DESC + ORDER BY v.name ASC OFFSET $1 LIMIT $2; ` rows, err := r.Db.Query(query, offset, limit) if err != nil { - return nil, fmt.Errorf("VaultRepo - GetPaginatedVaults - Query: %w", err) + return nil, 0, fmt.Errorf("VaultRepo - GetPaginatedVaults - Query: %w", err) } defer rows.Close() @@ -172,20 +222,20 @@ func (r VaultRepo) GetPaginatedVaults(page, limit int) ([]entity.Vault, error) { var wallet entity.Wallet err := rows.Scan( - &vault.Id, &vault.Name, &vault.Active, + &vault.Id, &vault.Name, &vault.Active, &vault.OwnerId, &vaultCategory.Id, &vaultCategory.Name, &vaultCategory.Theme, &wallet.Id, &wallet.Type, &wallet.Funded, &wallet.Key.Id, &wallet.Key.PublicKey, &wallet.Key.Weight, ) if err != nil { - return nil, fmt.Errorf("VaultRepo - GetPaginatedVaults - row.Scan: %w", err) + return nil, 0, fmt.Errorf("VaultRepo - GetPaginatedVaults - row.Scan: %w", err) } vault.Wallet = wallet - vault.VaultCategory = vaultCategory + vault.VaultCategory = &vaultCategory vaults = append(vaults, vault) } - return vaults, nil + return vaults, totalPages, nil } diff --git a/backend/internal/usecase/service/asset_service.go b/backend/internal/usecase/service/asset_service.go new file mode 100644 index 00000000..073ca428 --- /dev/null +++ b/backend/internal/usecase/service/asset_service.go @@ -0,0 +1,21 @@ +package service + +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/storage" +) + +type AssetService struct { + storageService storage.StorageService +} + +func NewAssetService(storage storage.StorageService) *AssetService { + return &AssetService{storageService: storage} +} + +func (s *AssetService) UploadFile(filename string, file []byte) (string, error) { + url, err := s.storageService.UploadFile(filename, file) + if err != nil { + return "", err + } + return url, nil +} diff --git a/backend/internal/usecase/users/reset_password.go b/backend/internal/usecase/users/reset_password.go new file mode 100644 index 00000000..f098e35d --- /dev/null +++ b/backend/internal/usecase/users/reset_password.go @@ -0,0 +1,7 @@ +package usecase + +// func (uc *UserUseCase) Execute(email string) error { +// // Generate reset token and save it in the database +// // Call the email service to send the reset link +// return nil +// } diff --git a/backend/internal/usecase/vault.go b/backend/internal/usecase/vault.go index da68e554..f6f514dc 100644 --- a/backend/internal/usecase/vault.go +++ b/backend/internal/usecase/vault.go @@ -19,12 +19,6 @@ func NewVaultUseCase(vRepo VaultRepoInterface, wRepo WalletRepoInterface) *Vault } func (uc *VaultUseCase) Create(data entity.Vault) (entity.Vault, error) { - wallet, err := uc.wRepo.CreateWalletWithKey(data.Wallet) - if err != nil { - return entity.Vault{}, fmt.Errorf("VaultUseCase - Create - uc.repo.CreateWalletWithKey(dist): %w", err) - } - data.Wallet.Id = wallet.Id - vault, err := uc.vRepo.CreateVault(data) if err != nil { return entity.Vault{}, fmt.Errorf("VaultUseCase - Create - uc.repo.CreateVault: %w", err) @@ -33,8 +27,8 @@ func (uc *VaultUseCase) Create(data entity.Vault) (entity.Vault, error) { return vault, nil } -func (uc *VaultUseCase) GetAll() ([]entity.Vault, error) { - vault, err := uc.vRepo.GetVaults() +func (uc *VaultUseCase) GetAll(isAll bool) ([]entity.Vault, error) { + vault, err := uc.vRepo.GetVaults(isAll) if err != nil { return nil, fmt.Errorf("VaultUseCase - GetAll - uc.repo.GetVault: %w", err) } @@ -69,11 +63,11 @@ func (uc *VaultUseCase) DeleteVault(data entity.Vault) (entity.Vault, error) { return vault, nil } -func (uc *VaultUseCase) GetPaginatedVaults(page, limit int) ([]entity.Vault, error) { - vault, err := uc.vRepo.GetPaginatedVaults(page, limit) +func (uc *VaultUseCase) GetPaginatedVaults(page, limit int) ([]entity.Vault, int, error) { + vault, totalPages, err := uc.vRepo.GetPaginatedVaults(page, limit) if err != nil { - return nil, fmt.Errorf("VaultUseCase - GetPaginatedVaults - uc.repo.GetPaginatedVaults: %w", err) + return nil, 0, fmt.Errorf("VaultUseCase - GetPaginatedVaults - uc.repo.GetPaginatedVaults: %w", err) } - return vault, nil + return vault, totalPages, nil } diff --git a/backend/internal/usecase/vault_test.go b/backend/internal/usecase/vault_test.go index 6d6644aa..66709017 100644 --- a/backend/internal/usecase/vault_test.go +++ b/backend/internal/usecase/vault_test.go @@ -45,7 +45,7 @@ func TestVaultUseCaseList(t *testing.T) { Wallet: entity.Wallet{ Type: entity.SponsorType, }, - VaultCategory: entity.VaultCategory{ + VaultCategory: &entity.VaultCategory{ Id: 1, Name: "Some Category", }, @@ -57,7 +57,7 @@ func TestVaultUseCaseList(t *testing.T) { Wallet: entity.Wallet{ Type: entity.IssuerType, }, - VaultCategory: entity.VaultCategory{ + VaultCategory: &entity.VaultCategory{ Id: 2, Name: "Another Category", }, @@ -68,7 +68,7 @@ func TestVaultUseCaseList(t *testing.T) { name: "list - two vaults", req: nil, mock: func() { - vr.EXPECT().GetVaults().Return([]entity.Vault{vault1, vault2}, nil) + vr.EXPECT().GetVaults(true).Return([]entity.Vault{vault1, vault2}, nil) }, res: []entity.Vault{vault1, vault2}, err: nil, @@ -77,7 +77,7 @@ func TestVaultUseCaseList(t *testing.T) { name: "list - empty", req: nil, mock: func() { - vr.EXPECT().GetVaults().Return([]entity.Vault{}, nil) + vr.EXPECT().GetVaults(true).Return([]entity.Vault{}, nil) }, res: []entity.Vault{}, err: nil, @@ -86,7 +86,7 @@ func TestVaultUseCaseList(t *testing.T) { name: "list - database error", req: nil, mock: func() { - vr.EXPECT().GetVaults().Return(nil, dbError) + vr.EXPECT().GetVaults(true).Return(nil, dbError) }, res: []entity.Vault(nil), err: dbError, @@ -98,7 +98,7 @@ func TestVaultUseCaseList(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.mock() - res, err := u.GetAll() + res, err := u.GetAll(true) require.EqualValues(t, tc.res, res) if tc.err == nil { @@ -119,7 +119,7 @@ func TestVaultUseCaseGetById(t *testing.T) { Wallet: entity.Wallet{ Type: entity.SponsorType, }, - VaultCategory: entity.VaultCategory{ + VaultCategory: &entity.VaultCategory{ Id: 1, Name: "Some Category", }, @@ -131,7 +131,7 @@ func TestVaultUseCaseGetById(t *testing.T) { Wallet: entity.Wallet{ Type: entity.IssuerType, }, - VaultCategory: entity.VaultCategory{ + VaultCategory: &entity.VaultCategory{ Id: 2, Name: "Another Category", }, @@ -192,7 +192,7 @@ func TestVaultUseCaseCreate(t *testing.T) { Wallet: entity.Wallet{ Type: entity.SponsorType, }, - VaultCategory: entity.VaultCategory{ + VaultCategory: &entity.VaultCategory{ Id: 1, Name: "Some Category", }, diff --git a/backend/main.go b/backend/main.go index da7dcc1d..d1f3b4cd 100644 --- a/backend/main.go +++ b/backend/main.go @@ -8,8 +8,11 @@ import ( "github.com/CheesecakeLabs/token-factory-v2/backend/config" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/app" "github.com/CheesecakeLabs/token-factory-v2/backend/internal/entity" + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/aws" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/kafka" + local "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/localstorage" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/postgres" + "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/storage" "github.com/CheesecakeLabs/token-factory-v2/backend/pkg/toml" ) @@ -29,6 +32,26 @@ func main() { } defer pg.Close() + // AWS Service or LocalStorage + var storageService storage.StorageService + if cfg.Deploy.DeployStage != "local" { + awsConn, err := aws.New(cfg.AWS) + if err != nil { + log.Fatalf("Failed to initialize AWS connection: %v", err) + } + storageService = awsConn + } else { + currentDir, err := os.Getwd() + if err != nil { + log.Fatalf("Failed to get current directory: %v", err) + } + + localStorage := &local.LocalStorage{ + BasePath: currentDir, // Use the absolute path + } + storageService = localStorage + } + // Kafka create keypair connection kpConn := kafka.New(cfg.Kafka, cfg.Kafka.CreateKpCfg.ConsumerTopics, cfg.Kafka.CreateKpCfg.ProducerTopic) err = kpConn.AttemptConnect() @@ -56,5 +79,23 @@ func main() { } go envConn.Run(cfg, entity.EnvelopeChannel) - app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer, tRepo) + // Kafka submit transaction connection + submitConn := kafka.New(cfg.Kafka, cfg.Kafka.SubmitTransactionCfg.ConsumerTopics, cfg.Kafka.SubmitTransactionCfg.ProducerTopic) + err = submitConn.AttemptConnect() + if err != nil { + fmt.Printf("Failed to connect to Kafka submit transaction topics %s\n", err) + os.Exit(1) + } + go submitConn.Run(cfg, entity.SubmitTransactionChannel) + + // Kafka Sign Transaction connection + signConn := kafka.New(cfg.Kafka, cfg.Kafka.SignTransactionCfg.ConsumerTopics, cfg.Kafka.SignTransactionCfg.ProducerTopic) + err = signConn.AttemptConnect() + if err != nil { + fmt.Printf("Failed to connect to Kafka sign transaction topics %s\n", err) + os.Exit(1) + } + go signConn.Run(cfg, entity.SignChannel) + + app.Run(cfg, pg, kpConn.Producer, horConn.Producer, envConn.Producer, submitConn.Producer, signConn.Producer, tRepo, storageService) } diff --git a/backend/migrations/000024_refact_permissions.up.sql b/backend/migrations/000024_refact_permissions.up.sql index 2d3176d7..c1e64d55 100644 --- a/backend/migrations/000024_refact_permissions.up.sql +++ b/backend/migrations/000024_refact_permissions.up.sql @@ -1,5 +1,5 @@ -DELETE FROM permission WHERE ID = 2; -DELETE FROM permission WHERE ID = 6; +DELETE FROM permission WHERE ID = 2 ; +DELETE FROM permission WHERE ID = 6 ; UPDATE permission SET name = 'Token management' WHERE ID = 3; UPDATE permission SET name = 'Transfer internally' WHERE ID = 4; UPDATE permission SET name = 'Transfer externally' WHERE ID = 5; diff --git a/backend/migrations/000029_asset_contract_id.down.sql b/backend/migrations/000029_asset_contract_id.down.sql new file mode 100644 index 00000000..e72b665f --- /dev/null +++ b/backend/migrations/000029_asset_contract_id.down.sql @@ -0,0 +1 @@ +ALTER TABLE asset DROP COLUMN contract_id; \ No newline at end of file diff --git a/backend/migrations/000029_asset_contract_id.up.sql b/backend/migrations/000029_asset_contract_id.up.sql new file mode 100644 index 00000000..56c3d750 --- /dev/null +++ b/backend/migrations/000029_asset_contract_id.up.sql @@ -0,0 +1 @@ +ALTER TABLE asset ADD COLUMN contract_id varchar(255); \ No newline at end of file diff --git a/backend/migrations/000030_add_contract_compound.down.sql b/backend/migrations/000030_add_contract_compound.down.sql new file mode 100644 index 00000000..dd7aeaf2 --- /dev/null +++ b/backend/migrations/000030_add_contract_compound.down.sql @@ -0,0 +1 @@ +ALTER TABLE contracts DROP COLUMN compound; \ No newline at end of file diff --git a/backend/migrations/000030_add_contract_compound.up.sql b/backend/migrations/000030_add_contract_compound.up.sql new file mode 100644 index 00000000..c8bfa6c7 --- /dev/null +++ b/backend/migrations/000030_add_contract_compound.up.sql @@ -0,0 +1 @@ +ALTER TABLE contracts ADD COLUMN compound INT DEFAULT 0; \ No newline at end of file diff --git a/backend/migrations/000031_add_vault_owner.down.sql b/backend/migrations/000031_add_vault_owner.down.sql new file mode 100644 index 00000000..e4128470 --- /dev/null +++ b/backend/migrations/000031_add_vault_owner.down.sql @@ -0,0 +1 @@ +ALTER TABLE vault DROP COLUMN owner_id; \ No newline at end of file diff --git a/backend/migrations/000031_add_vault_owner.up.sql b/backend/migrations/000031_add_vault_owner.up.sql new file mode 100644 index 00000000..fc7d5013 --- /dev/null +++ b/backend/migrations/000031_add_vault_owner.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE vault +ADD COLUMN owner_id INT, +ADD CONSTRAINT fk_vault_owner_id + FOREIGN KEY (owner_id) + REFERENCES useraccount(id); \ No newline at end of file diff --git a/backend/migrations/000032_vault_catogory_null.up.sql b/backend/migrations/000032_vault_catogory_null.up.sql new file mode 100644 index 00000000..b201256f --- /dev/null +++ b/backend/migrations/000032_vault_catogory_null.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE Vault +ALTER COLUMN vault_category_id DROP NOT NULL; \ No newline at end of file diff --git a/backend/migrations/000033_add_contract_created_at.down.sql b/backend/migrations/000033_add_contract_created_at.down.sql new file mode 100644 index 00000000..eb790762 --- /dev/null +++ b/backend/migrations/000033_add_contract_created_at.down.sql @@ -0,0 +1 @@ +ALTER TABLE contracts DROP COLUMN created_at; \ No newline at end of file diff --git a/backend/migrations/000033_add_contract_created_at.up.sql b/backend/migrations/000033_add_contract_created_at.up.sql new file mode 100644 index 00000000..c48449a5 --- /dev/null +++ b/backend/migrations/000033_add_contract_created_at.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE contracts +ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW() \ No newline at end of file diff --git a/backend/migrations/000034_contract_history.down.sql b/backend/migrations/000034_contract_history.down.sql new file mode 100644 index 00000000..a48a469b --- /dev/null +++ b/backend/migrations/000034_contract_history.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS Contracts; \ No newline at end of file diff --git a/backend/migrations/000034_contract_history.up.sql b/backend/migrations/000034_contract_history.up.sql new file mode 100644 index 00000000..17fb4599 --- /dev/null +++ b/backend/migrations/000034_contract_history.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE ContractsHistory ( + id SERIAL PRIMARY KEY, + contract_id INT NOT NULL, + deposited_at TIMESTAMP NOT NULL DEFAULT NOW(), + deposit_amount FLOAT NOT NULL, + withdrawn_at TIMESTAMP, + withdraw_amount FLOAT, + user_id INT NOT NULL, + FOREIGN KEY (contract_id) REFERENCES Contracts(id), + FOREIGN KEY (user_id) REFERENCES UserAccount(id) +); \ No newline at end of file diff --git a/backend/migrations/000035_add_contract_permissions.down.sql b/backend/migrations/000035_add_contract_permissions.down.sql new file mode 100644 index 00000000..7cbb9c77 --- /dev/null +++ b/backend/migrations/000035_add_contract_permissions.down.sql @@ -0,0 +1,22 @@ +DO $$ +DECLARE + permission_id INT; +BEGIN + -- Revert 'Invest in the certificate' + SELECT id INTO permission_id FROM Permission WHERE name = 'Invest in the certificate'; + DELETE FROM RolePermissionJunction WHERE permission_id = permission_id; + DELETE FROM Operation WHERE permission_id = permission_id; + DELETE FROM Permission WHERE id = permission_id; + + -- Revert 'Create wallet' + SELECT id INTO permission_id FROM Permission WHERE name = 'Create wallet'; + DELETE FROM RolePermissionJunction WHERE permission_id = permission_id; + DELETE FROM Operation WHERE permission_id = permission_id; + DELETE FROM Permission WHERE id = permission_id; + + -- Revert 'Create certificates' + SELECT id INTO permission_id FROM Permission WHERE name = 'Create certificates'; + DELETE FROM RolePermissionJunction WHERE permission_id = permission_id; + DELETE FROM Operation WHERE permission_id = permission_id; + DELETE FROM Permission WHERE id = permission_id; +END $$; diff --git a/backend/migrations/000035_add_contract_permissions.up.sql b/backend/migrations/000035_add_contract_permissions.up.sql new file mode 100644 index 00000000..8451c014 --- /dev/null +++ b/backend/migrations/000035_add_contract_permissions.up.sql @@ -0,0 +1,27 @@ +DO $$ +DECLARE + permission_id INT; +BEGIN + INSERT INTO Permission (name, description) VALUES ('Create certificates', '') RETURNING id INTO permission_id; + EXECUTE format('INSERT INTO Operation (name, description, permission_id, action) VALUES (%L, %L, %s, %L)', 'Create certificates', '', permission_id, 'create-certificates'); + EXECUTE format('INSERT INTO RolePermissionJunction (role_id, permission_id) VALUES (%s, %s)', 1, permission_id); +END $$; + +DO $$ +DECLARE + permission_id INT; +BEGIN + INSERT INTO Permission (name, description) VALUES ('Create wallet', '') RETURNING id INTO permission_id; + EXECUTE format('INSERT INTO Operation (name, description, permission_id, action) VALUES (%L, %L, %s, %L)', 'Create wallet', '', permission_id, 'create-wallet'); + EXECUTE format('INSERT INTO RolePermissionJunction (role_id, permission_id) VALUES (%s, %s)', 1, permission_id); +END $$; + + +DO $$ +DECLARE + permission_id INT; +BEGIN + INSERT INTO Permission (name, description) VALUES ('Invest in the certificate', '') RETURNING id INTO permission_id; + EXECUTE format('INSERT INTO Operation (name, description, permission_id, action) VALUES (%L, %L, %s, %L)', 'Invest in the certificate', '', permission_id, 'invest-certificate'); + EXECUTE format('INSERT INTO RolePermissionJunction (role_id, permission_id) VALUES (%s, %s)', 1, permission_id); +END $$; diff --git a/backend/migrations/000036_add_idx_wallet_table.down.sql b/backend/migrations/000036_add_idx_wallet_table.down.sql new file mode 100644 index 00000000..3c1e7d3b --- /dev/null +++ b/backend/migrations/000036_add_idx_wallet_table.down.sql @@ -0,0 +1,15 @@ +-- Removing indexes added for optimization + +-- Asset Table +DROP INDEX idx_asset_name; +DROP INDEX idx_asset_asset_type; +DROP INDEX idx_asset_code; +DROP INDEX idx_asset_distributor_id; +DROP INDEX idx_asset_issuer_id; + +-- Wallet Table +DROP INDEX idx_wallet_type; + +-- Key Table +DROP INDEX idx_key_public_key; +DROP INDEX idx_key_wallet_id; diff --git a/backend/migrations/000036_add_idx_wallet_table.up.sql b/backend/migrations/000036_add_idx_wallet_table.up.sql new file mode 100644 index 00000000..3ac06f7d --- /dev/null +++ b/backend/migrations/000036_add_idx_wallet_table.up.sql @@ -0,0 +1,13 @@ +-- Asset Table +CREATE INDEX idx_asset_name ON Asset(name); +CREATE INDEX idx_asset_asset_type ON Asset(asset_type); +CREATE INDEX idx_asset_code ON Asset(code); +CREATE INDEX idx_asset_distributor_id ON Asset(distributor_id); +CREATE INDEX idx_asset_issuer_id ON Asset(issuer_id); + +-- Wallet Table +CREATE INDEX idx_wallet_type ON Wallet(type); + +-- Key Table +CREATE INDEX idx_key_public_key ON Key(public_key); +CREATE INDEX idx_key_wallet_id ON Key(wallet_id); diff --git a/backend/migrations/000037_alter_image_column.down.sql b/backend/migrations/000037_alter_image_column.down.sql new file mode 100644 index 00000000..8579483e --- /dev/null +++ b/backend/migrations/000037_alter_image_column.down.sql @@ -0,0 +1,2 @@ +-- Migration Down Script +ALTER TABLE asset ALTER COLUMN image TYPE BYTEA USING image::BYTEA; diff --git a/backend/migrations/000037_alter_image_column.up.sql b/backend/migrations/000037_alter_image_column.up.sql new file mode 100644 index 00000000..dfb9fda5 --- /dev/null +++ b/backend/migrations/000037_alter_image_column.up.sql @@ -0,0 +1,2 @@ +-- Migration Up Script +ALTER TABLE asset ALTER COLUMN image TYPE VARCHAR(255); diff --git a/backend/migrations/000038_update_asset_images.up.sql b/backend/migrations/000038_update_asset_images.up.sql new file mode 100644 index 00000000..2032101f --- /dev/null +++ b/backend/migrations/000038_update_asset_images.up.sql @@ -0,0 +1 @@ +UPDATE asset SET image = null \ No newline at end of file diff --git a/backend/migrations/000039_testnet_reset.up.sql b/backend/migrations/000039_testnet_reset.up.sql new file mode 100644 index 00000000..66b9fc60 --- /dev/null +++ b/backend/migrations/000039_testnet_reset.up.sql @@ -0,0 +1,15 @@ +TRUNCATE TABLE logtransactions CASCADE; +TRUNCATE TABLE contractshistory CASCADE; +TRUNCATE TABLE contracts CASCADE; +TRUNCATE TABLE vault CASCADE; +TRUNCATE TABLE vaultcategory CASCADE; +TRUNCATE TABLE asset CASCADE; +TRUNCATE TABLE key CASCADE; +TRUNCATE TABLE wallet CASCADE; +TRUNCATE TABLE useraccount CASCADE; +TRUNCATE TABLE toml CASCADE; + +ALTER SEQUENCE public.key_id_seq RESTART 1; +ALTER SEQUENCE public.wallet_id_seq RESTART 1; +ALTER SEQUENCE public.asset_id_seq RESTART 1; + diff --git a/backend/pkg/aws/aws_ses_service.go b/backend/pkg/aws/aws_ses_service.go new file mode 100644 index 00000000..53bb4f53 --- /dev/null +++ b/backend/pkg/aws/aws_ses_service.go @@ -0,0 +1,38 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ses" +) + +func (s *AwsConnection) SendEmail(to, subject, body string) error { + input := &ses.SendEmailInput{ + Destination: &ses.Destination{ + ToAddresses: []*string{ + aws.String(to), + }, + }, + Message: &ses.Message{ + Body: &ses.Body{ + Html: &ses.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(body), + }, + }, + Subject: &ses.Content{ + Charset: aws.String("UTF-8"), + Data: aws.String(subject), + }, + }, + Source: aws.String("your-email@example.com"), // TODO: Change this + } + + _, err := s.emailClient.SendEmail(input) + if err != nil { + return fmt.Errorf("failed to send email: %v", err) + } + + return nil +} diff --git a/backend/pkg/aws/s3.go b/backend/pkg/aws/s3.go new file mode 100644 index 00000000..831fbdf5 --- /dev/null +++ b/backend/pkg/aws/s3.go @@ -0,0 +1,28 @@ +package aws + +import ( + "bytes" + "path/filepath" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3/s3manager" +) + +func (s *AwsConnection) uploadToS3(filename string, file []byte) (string, error) { + uploader := s3manager.NewUploader(s.session) + + if filepath.Ext(filename) != ".png" { + filename += ".png" + } + + result, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucketName), + Key: aws.String(filename), + Body: bytes.NewReader(file), + }) + if err != nil { + return "", err + } + + return result.Location, nil +} diff --git a/backend/pkg/aws/server.go b/backend/pkg/aws/server.go new file mode 100644 index 00000000..c2619bea --- /dev/null +++ b/backend/pkg/aws/server.go @@ -0,0 +1,36 @@ +package aws + +import ( + "github.com/CheesecakeLabs/token-factory-v2/backend/config" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ses" +) + +type AwsConnection struct { + session *session.Session + emailClient *ses.SES + bucketName string +} + +// Initialize a session with AWS +func New(cfg config.AWS) (*AwsConnection, error) { + sess, err := session.NewSession( + &aws.Config{ + Region: aws.String(cfg.AwsRegion), + }, + ) + if err != nil { + return &AwsConnection{}, err + } + + return &AwsConnection{ + session: sess, + emailClient: ses.New(sess), + bucketName: cfg.BucketName, + }, nil +} + +func (s *AwsConnection) UploadFile(filename string, file []byte) (string, error) { + return s.uploadToS3(filename, file) +} diff --git a/backend/pkg/kafka/deserializer.go b/backend/pkg/kafka/deserializer.go index b2b47cde..c53ef4db 100644 --- a/backend/pkg/kafka/deserializer.go +++ b/backend/pkg/kafka/deserializer.go @@ -23,6 +23,10 @@ func (d *Deserializer) DeserializeMessage(msg *kafka.Message, chanName string) ( return d.deserializeHorizonMessage(msg) case entity.EnvelopeChannel: return d.deserializeEnvelopeMessage(msg) + case entity.SignChannel: + return d.deserializeEnvelopSorobanTransaction(msg) + case entity.SubmitTransactionChannel: + return d.deserializeEnvelopeMessage(msg) default: return nil, fmt.Errorf("invalid channel name: %s", chanName) } @@ -55,6 +59,15 @@ func (d *Deserializer) deserializeEnvelopeMessage(msg *kafka.Message) (entity.En return data, nil } +func (d *Deserializer) deserializeEnvelopSorobanTransaction(msg *kafka.Message) (entity.SignTransactionRequest, error) { + data := entity.SignTransactionRequest{} + err := d.unmarshalMessage(msg, &data) + if err != nil { + return data, fmt.Errorf("failed to deserialize envelope response: %w", err) + } + return data, nil +} + func (d *Deserializer) unmarshalMessage(msg *kafka.Message, v interface{}) error { if d.schemaEnabled { return d.exec.DeserializeInto(*msg.TopicPartition.Topic, msg.Value, v) diff --git a/backend/pkg/kafka/producer.go b/backend/pkg/kafka/producer.go index 9f47623e..dcea4570 100644 --- a/backend/pkg/kafka/producer.go +++ b/backend/pkg/kafka/producer.go @@ -15,7 +15,7 @@ type Producer struct { func (p *Producer) Produce(key string, value interface{}) error { valueMarshalled, err := json.Marshal(value) if err != nil { - return fmt.Errorf("Producer - Produce - json.Marshal: %v\n", err) + return fmt.Errorf("Producer - Produce - json.Marshal: %v", err) } err = p.exec.Produce(&kafka.Message{ @@ -23,8 +23,9 @@ func (p *Producer) Produce(key string, value interface{}) error { TopicPartition: kafka.TopicPartition{Topic: &p.Topic, Partition: kafka.PartitionAny}, Value: valueMarshalled, }, nil) + if err != nil { - return fmt.Errorf("Producer - Produce - p.exec.Produce: %v\n", err) + return fmt.Errorf("Producer - Produce - p.exec.Produce: %v", err) } return nil diff --git a/backend/pkg/localstorage/storage.go b/backend/pkg/localstorage/storage.go new file mode 100644 index 00000000..4f16411d --- /dev/null +++ b/backend/pkg/localstorage/storage.go @@ -0,0 +1,22 @@ +package local + +import ( + "os" + "path/filepath" +) + +type LocalStorage struct { + BasePath string +} + +func (l *LocalStorage) UploadFile(filename string, file []byte) (string, error) { + if filepath.Ext(filename) != ".png" { + filename += ".png" + } + fullPath := filepath.Join(l.BasePath, filename) + err := os.WriteFile(fullPath, file, 0o666) + if err != nil { + return "", err + } + return fullPath, nil +} diff --git a/backend/pkg/storage/storage.go b/backend/pkg/storage/storage.go new file mode 100644 index 00000000..5cd75929 --- /dev/null +++ b/backend/pkg/storage/storage.go @@ -0,0 +1,5 @@ +package storage + +type StorageService interface { + UploadFile(filename string, file []byte) (string, error) +} diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 5f50bef9..3b1d83f4 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -3,7 +3,7 @@ version: "3.7" services: postgres: container_name: postgres - profiles: ["all", "starlabs", "tests", "kafka"] + profiles: [ "all", "starlabs", "tests", "kafka" ] image: postgres environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} @@ -19,14 +19,14 @@ services: test: [ "CMD-SHELL", - "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'", + "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'" ] interval: 10s timeout: 3s retries: 3 tfv2_frontend: - profiles: ["all", "frontend"] + profiles: [ "all", "frontend" ] container_name: tfv2_frontend build: context: ./frontend @@ -45,7 +45,7 @@ services: backend: platform: linux/amd64 - profiles: ["all", "tests"] + profiles: [ "all", "tests" ] container_name: token-factory-v2 build: context: ./backend @@ -61,7 +61,7 @@ services: stellar-kms: platform: linux/amd64 - profiles: ["all", "starlabs", "tests"] + profiles: [ "all", "starlabs", "tests" ] container_name: kms build: context: ./stellar-kms @@ -76,13 +76,15 @@ services: KAFKA_CREATE_KP_PRODUCER_TOPIC: ${KMS_KAFKA_PRODUCER_TOPIC:-generatedKeypairs} KAFKA_SIGN_CONSUMER_TOPICS: ${KAFKA_SIGN_CONSUMER_TOPICS:-signEnvelope} KAFKA_SIGN_PRODUCER_TOPIC: ${KAFKA_SIGN_PRODUCER_TOPIC:-signedEnvelopes} - LOG_LEVEL: ${LOG_LEVEL:-debug} + KAFKA_SIGN_SOROBAN_CONSUMER_TOPICS: ${KAFKA_SIGN_SOROBAN_CONSUMER_TOPICS:-signSorobanEnvelope} + KAFKA_SIGN_SOROBAN_PRODUCER_TOPIC: ${KAFKA_SIGN_SOROBAN_PRODUCER_TOPIC:-signedSorobanEnvelopes} PG_HOST: ${PG_HOST:-postgres} PG_PORT: ${PG_PORT:-5432} PG_USER: ${PG_USER:-postgres} PG_PASSWORD: ${PG_PASSWORD:-password} PG_DB_NAME: ${PG_DATABASE:-postgres} MASTER_KEY: ${MASTER_KEY:-da52f130e6fd477256b0c554aba89503} + LOG_LEVEL: ${LOG_LEVEL:-debug} depends_on: postgres: condition: service_healthy @@ -90,7 +92,7 @@ services: condition: service_healthy zoo1: - profiles: ["all", "starlabs", "tests", "kafka"] + profiles: [ "all", "starlabs", "tests", "kafka" ] image: confluentinc/cp-zookeeper:7.3.2 hostname: zoo1 container_name: zoo1 @@ -102,7 +104,7 @@ services: ZOOKEEPER_SERVERS: zoo1:2888:3888 kafka1: - profiles: ["all", "starlabs", "tests", "kafka"] + profiles: [ "all", "starlabs", "tests", "kafka" ] image: confluentinc/cp-kafka:7.3.2 hostname: kafka1 container_name: kafka1 @@ -135,7 +137,7 @@ services: #Kafka - Schema Registry# schema_registry: - profiles: ["all", "kafka", "starlabs", "kms"] + profiles: [ "all", "kafka", "starlabs", "kms" ] image: confluentinc/cp-schema-registry:7.1.2 container_name: schema_registry ports: @@ -158,10 +160,10 @@ services: #Kafka - Init# kafka_init: - profiles: ["all", "kafka", "starlabs", "kms"] + profiles: [ "all", "kafka", "starlabs", "kms" ] image: confluentinc/cp-kafka:7.1.2 container_name: kafka_init - entrypoint: ["/bin/sh", "-c"] + entrypoint: [ "/bin/sh", "-c" ] environment: KAFKA_CREATE_KP_PRODUCER_TOPIC: ${KAFKA_CREATE_KP_PRODUCER_TOPIC:-generateKeypair} KAFKA_CREATE_KP_CONSUMER_TOPICS: ${KAFKA_CREATE_KP_CONSUMER_TOPICS:-generatedKeypairs} @@ -169,6 +171,8 @@ services: KAFKA_ENVELOPE_CONSUMER_TOPICS: ${KAFKA_ENVELOPE_CONSUMER_TOPICS:-submitResponse} KAFKA_HORIZON_PRODUCER_TOPIC: ${KAFKA_HORIZON_PRODUCER_TOPIC:-horizonRequest} KAFKA_HORIZON_CONSUMER_TOPICS: ${KAFKA_HORIZON_CONSUMER_TOPICS:-horizonResponse} + SIGNED_SOROBAN_PRODUCER_TOPIC: ${SIGNED_SOROBAN_PRODUCER_TOPIC:-signSorobanEnvelope} + SIGNED_SOROBAN_CONSUMER_TOPICS: ${SIGNED_SOROBAN_CONSUMER_TOPICS:-signedSorobanEnvelopes} KAFKA_HORIZON_SIGNED_ENVELOPES_TOPICS: ${KAFKA_HORIZON_SIGNED_ENVELOPES_TOPICS:-signedEnvelopes} KAFKA_HORIZON_SIGN_ENVLOPES_TOPICS: ${KAFKA_HORIZON_SIGN_ENVLOPES_TOPICS:-signEnvelope} depends_on: @@ -186,12 +190,14 @@ services: kafka-topics --create --topic $$KAFKA_HORIZON_CONSUMER_TOPICS --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:19092 kafka-topics --create --topic $$KAFKA_HORIZON_SIGNED_ENVELOPES_TOPICS --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:19092 kafka-topics --create --topic $$KAFKA_HORIZON_SIGN_ENVLOPES_TOPICS --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:19092 + kafka-topics --create --topic $$SIGNED_SOROBAN_PRODUCER_TOPIC --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:19092 + kafka-topics --create --topic $$SIGNED_SOROBAN_CONSUMER_TOPICS --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server kafka1:19092 kafka1 cub kafka-ready -b kafka1:129092 1 20 " # Kafka - UI# kafka_ui: - profiles: ["all", "kafka", "starlabs", "kms"] + profiles: [ "all", "kafka", "starlabs", "kms" ] image: provectuslabs/kafka-ui container_name: kafka_ui ports: @@ -211,7 +217,7 @@ services: #Starlabs Service# starlabs: platform: linux/amd64 - profiles: ["all", "starlabs", "tests"] + profiles: [ "all", "starlabs", "tests" ] container_name: starlabs build: context: ./starlabs @@ -229,12 +235,13 @@ services: KAFKA_SUBMIT_CONSUMER_TOPICS: ${KAFKA_SUBMIT_CONSUMER_TOPICS:-signedEnvelopes} KAFKA_SUBMIT_PRODUCER_TOPIC: ${KAFKA_SUBMIT_PRODUCER_TOPIC:-submitResponse} LOG_LEVEL: ${LOG_LEVEL:-debug} + depends_on: kafka1: condition: service_healthy integration: - profiles: ["tests"] + profiles: [ "tests" ] platform: linux/amd64 build: context: ./backend