From 7f9ba60dd6e4ebb95c7137a2baa962c0502332c1 Mon Sep 17 00:00:00 2001 From: Wilfredo Alcala Date: Wed, 25 Jan 2023 17:46:55 -0400 Subject: [PATCH 01/35] STR-297 :: get user on refresh (#97) * get user on refresh * improve error handling --- api/handler/http_error.go | 7 +++++++ api/handler/login.go | 7 ++++--- api/handler/user.go | 4 ++++ pkg/service/auth.go | 29 +++++++++++++++++++---------- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/api/handler/http_error.go b/api/handler/http_error.go index f3fddc95..5a339812 100644 --- a/api/handler/http_error.go +++ b/api/handler/http_error.go @@ -83,3 +83,10 @@ func Conflict(c echo.Context, message ...string) error { } return c.JSON(http.StatusConflict, JSONError{Message: "Conflict", Code: "CONFLICT"}) } + +func LinkExpired(c echo.Context, message ...string) error { + if len(message) > 0 { + return c.JSON(http.StatusForbidden, JSONError{Message: strings.Join(message, " "), Code: "LINK_EXPIRED"}) + } + return c.JSON(http.StatusForbidden, JSONError{Message: "Forbidden", Code: "LINK_EXPIRED"}) +} diff --git a/api/handler/login.go b/api/handler/login.go index be08c07a..c943a85f 100644 --- a/api/handler/login.go +++ b/api/handler/login.go @@ -93,7 +93,7 @@ func (l login) RefreshToken(c echo.Context) error { return Unauthorized(c) } - jwt, err := l.Service.RefreshToken(cookie.Value, body.WalletAddress) + resp, err := l.Service.RefreshToken(cookie.Value, body.WalletAddress) if err != nil { if strings.Contains(err.Error(), "wallet address not associated with this user") { return BadRequestError(c, "wallet address not associated with this user") @@ -102,14 +102,15 @@ func (l login) RefreshToken(c echo.Context) error { LogStringError(c, err, "login: refresh token") return BadRequestError(c, "Invalid or expired token") } + // set auth in cookies - err = SetAuthCookies(c, jwt) + err = SetAuthCookies(c, resp.JWT) if err != nil { LogStringError(c, err, "RefreshToken: unable to set auth cookies") return InternalError(c) } - return c.JSON(http.StatusOK, jwt) + return c.JSON(http.StatusOK, resp) } // logout diff --git a/api/handler/user.go b/api/handler/user.go index ac76b117..92e9fea1 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -108,6 +108,10 @@ func (u user) VerifyEmail(c echo.Context) error { return Conflict(c) } + if strings.Contains(err.Error(), "link expired") { + return LinkExpired(c, "Link expired, please request a new one") + } + LogStringError(c, err, "user: email verification") return InternalError(c, "Unable to send email verification") } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 48f256e3..3c24fd51 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -55,7 +55,7 @@ type Auth interface { GenerateJWT(model.Device) (JWT, error) ValidateAPIKey(key string) bool - RefreshToken(token string, walletAddress string) (JWT, error) + RefreshToken(token string, walletAddress string) (UserCreateResponse, error) InvalidateRefreshToken(token string) error } @@ -212,11 +212,13 @@ func (a auth) InvalidateRefreshToken(refreshToken string) error { return a.repos.Auth.Delete(common.ToSha256(refreshToken)) } -func (a auth) RefreshToken(refreshToken string, walletAddress string) (JWT, error) { +func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreateResponse, error) { + resp := UserCreateResponse{} + // get user id from refresh token userId, err := a.repos.Auth.GetUserIdFromRefreshToken(common.ToSha256(refreshToken)) if err != nil { - return JWT{}, common.StringError(err) + return resp, common.StringError(err) } // verify wallet address @@ -224,34 +226,41 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (JWT, erro instrument, err := a.repos.Instrument.GetWallet(walletAddress) if err != nil { if strings.Contains(err.Error(), "not found") { - return JWT{}, common.StringError(errors.New("wallet address not associated with this user: " + walletAddress)) + return resp, common.StringError(errors.New("wallet address not associated with this user: " + walletAddress)) } - return JWT{}, common.StringError(err) + return resp, common.StringError(err) } if instrument.UserID != userId { - return JWT{}, common.StringError(errors.New("wallet address not associated with this user: " + walletAddress)) + return resp, common.StringError(errors.New("wallet address not associated with this user: " + walletAddress)) } // get device device, err := a.repos.Device.GetByUserId(userId) if err != nil { - return JWT{}, common.StringError(err) + return resp, common.StringError(err) } // create new jwt jwt, err := a.GenerateJWT(device) if err != nil { - return JWT{}, common.StringError(err) + return resp, common.StringError(err) } + resp.JWT = jwt // delete old refresh token err = a.InvalidateRefreshToken(refreshToken) if err != nil { - return JWT{}, common.StringError(err) + return resp, common.StringError(err) + } + + user, err := a.repos.User.GetById(instrument.UserID) + if err != nil { + return resp, common.StringError(err) } + resp.User = user - return jwt, nil + return resp, nil } func verifyWalletAuthentication(request model.WalletSignaturePayloadSigned) error { From df28fc596d2f32652e5d27ca00d2c0c88c5a62cb Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 25 Jan 2023 15:57:46 -0700 Subject: [PATCH 02/35] adding platform DB stuff --- ...-member_role_member-role_invite_apikey.sql | 115 ++++++++++++++++++ pkg/model/entity.go | 65 +++++++++- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql diff --git a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql new file mode 100644 index 00000000..a8d85291 --- /dev/null +++ b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql @@ -0,0 +1,115 @@ +------------------------------------------------------------------------- +-- +goose Up + +------------------------------------------------------------------------- +-- PLATFORM ----------------------------------------------------- +CREATE TABLE platform ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users + name TEXT NOT NULL, + description TEXT NOT NULL, + domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) + ip_addresses TEXT[] DEFAULT NULL, -- define which API ips can make calls (API-to-API) +); + +------------------------------------------------------------------------- +-- MEMBER ----------------------------------------------------- +CREATE TABLE member ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + email TEXT NOT NULL, + password TEXT NOT NULL, -- how do we maintain this? +); + +------------------------------------------------------------------------- +-- PLATFORM_MEMBER ----------------------------------------------------- +CREATE TABLE platform_member ( + platform_id UUID REFERENCES platform (id), + member_id UUID REFERENCES member (id) +); + +CREATE UNIQUE INDEX platform_member_platform_id_member_id_idx ON platform_member(platform_id, member_id); + +------------------------------------------------------------------------- +-- ROLE ----------------------------------------------------- +CREATE TABLE role ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + name TEXT NOT NULL, +); + +------------------------------------------------------------------------- +-- MEMBER_ROLE ----------------------------------------------------- +CREATE TABLE member_role ( + member_id UUID REFERENCES member (id) + role_id UUID REFERENCES role (id), +); + +CREATE UNIQUE INDEX member_role_member_id_role_id_idx ON member_role(member_id, role_id); + +------------------------------------------------------------------------- +-- INVITE ----------------------------------------------------- +CREATE TABLE invite ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + expired_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + email TEXT NOT NULL, + invited_by UUID REFERENCES member (id), + platform_id UUID REFERENCES platform (id), +); + +------------------------------------------------------------------------- +-- APIKEY ----------------------------------------------------- +CREATE TABLE apikey ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + type TEXT NOT NULL, -- [public,private] for now all public? + data TEXT NOT NULL, -- the key itself + description TEXT NOT NULL, + created_by UUID REFERENCES member (id), + platform_id UUID REFERENCES platform (id), +); + + +------------------------------------------------------------------------- +-- +goose Down + +------------------------------------------------------------------------- +-- CONTACT_PLATFORM ----------------------------------------------------- +DROP TABLE IF EXISTS platform; + +------------------------------------------------------------------------- +-- MEMBER ----------------------------------------------------- +DROP TABLE IF EXISTS member; + +------------------------------------------------------------------------- +-- PLATFORM_MEMBER ----------------------------------------------------- +DROP TABLE IF EXISTS platform_member; + +------------------------------------------------------------------------- +-- ROLE ----------------------------------------------------- +DROP TABLE IF EXISTS role; + +------------------------------------------------------------------------- +-- MEMBER_ROLE ----------------------------------------------------- +DROP TABLE IF EXISTS member_role; + +------------------------------------------------------------------------- +-- INVITE ----------------------------------------------------- +DROP TABLE IF EXISTS invite; + +------------------------------------------------------------------------- +-- APIKEY ----------------------------------------------------- +DROP TABLE IF EXISTS apikey; \ No newline at end of file diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 0908e048..0a53dcd2 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -23,7 +23,7 @@ type User struct { } // See PLATFORM in Migrations 0001 -type Platform struct { +type Platform_DEPRECATED struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` @@ -205,3 +205,66 @@ type AuthStrategy struct { func (a AuthStrategy) MarshalBinary() ([]byte, error) { return json.Marshal(a) } + +type Platform struct { + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + ActivatedAt *time.Time `json:"activatedAt,omitempty" db:"activated_at"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Domains pq.StringArray `json:"domains" db:"domains"` + IPAddresses pq.StringArray `json:"ipAddresses" db:"ip_addresses"` +} + +type Member struct { + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + Email string `json:"email" db:"email"` + Password string `json:"password" db:"password"` +} + +type PlatformMember struct { + PlatformID string `json:"platformId" db:"platform_id"` + MemberID string `json:"memberId" db:"member_id"` +} + +type Role struct { + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + Name string `json:"name" db:"name"` +} + +type MemberRole struct { + MemberID string `json:"memberId" db:"member_id"` + RoleID string `json:"roleId" db:"role_id"` +} + +type Invite struct { + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + ExpiredAt *time.Time `json:"expiredAt,omitempty" db:"expired_at"` + AcceptedAt *time.Time `json:"acceptedAt,omitempty" db:"accepted_at"` + Email string `json:"email" db:"email"` + InvitedBy string `json:"invitedBy" db:"invited_by"` + PlatformID string `json:"platformId" db:"platform_id"` +} + +type Apikey struct { + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + Type string `json:"type" db:"type"` + Data string `json:"data" db:"data"` + Description string `json:"description" db:"description"` + CreatedBy string `json:"createdBy" db:"created_by"` + PlatformID string `json:"platformId" db:"platform_id"` +} From 7633c73b26f24c3ec31ef322f0ecde0c86d38aa9 Mon Sep 17 00:00:00 2001 From: Wilfredo Alcala Date: Thu, 26 Jan 2023 16:00:40 -0400 Subject: [PATCH 03/35] base64 encode nonce (#91) * encode and decode nonce * delete test message * fix sign_test.go * Update api/handler/login.go Co-authored-by: Auroter <7332587+Auroter@users.noreply.github.com> * Update api/handler/user.go Co-authored-by: Auroter <7332587+Auroter@users.noreply.github.com> * minor fix * Update pkg/internal/common/sign_test.go Co-authored-by: Auroter <7332587+Auroter@users.noreply.github.com> Co-authored-by: Auroter <7332587+Auroter@users.noreply.github.com> --- api/handler/login.go | 12 +++++++++++- api/handler/user.go | 9 +++++++++ pkg/internal/common/sign_test.go | 13 ++++++++++--- pkg/service/auth.go | 2 -- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/api/handler/login.go b/api/handler/login.go index c943a85f..dd1657e9 100644 --- a/api/handler/login.go +++ b/api/handler/login.go @@ -1,6 +1,7 @@ package handler import ( + b64 "encoding/base64" "net/http" "strings" @@ -41,7 +42,8 @@ func (l login) NoncePayload(c echo.Context) error { return InternalError(c) } - return c.JSON(http.StatusOK, payload) + encodedNonce := b64.StdEncoding.EncodeToString([]byte(payload.Nonce)) + return c.JSON(http.StatusOK, map[string]string{"nonce": encodedNonce}) } func (l login) VerifySignature(c echo.Context) error { @@ -56,6 +58,14 @@ func (l login) VerifySignature(c echo.Context) error { return InvalidPayloadError(c, err) } + // base64 decode nonce + decodedNonce, _ := b64.URLEncoding.DecodeString(body.Nonce) + if err != nil { + LogStringError(c, err, "login: verify signature decode nonce") + return BadRequestError(c) + } + body.Nonce = string(decodedNonce) + resp, err := l.Service.VerifySignedPayload(body) if err != nil && strings.Contains(err.Error(), "unknown device") { return Unprocessable(c) diff --git a/api/handler/user.go b/api/handler/user.go index 92e9fea1..aa7f25ab 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -1,6 +1,7 @@ package handler import ( + b64 "encoding/base64" "net/http" "strings" @@ -43,6 +44,14 @@ func (u user) Create(c echo.Context) error { return InvalidPayloadError(c, err) } + // base64 decode nonce + decodedNonce, _ := b64.URLEncoding.DecodeString(body.Nonce) + if err != nil { + LogStringError(c, err, "user: create user decode nonce") + return BadRequestError(c) + } + body.Nonce = string(decodedNonce) + resp, err := u.userService.Create(body) if err != nil { if strings.Contains(err.Error(), "wallet already associated with user") { diff --git a/pkg/internal/common/sign_test.go b/pkg/internal/common/sign_test.go index 85af266a..30c8be7d 100644 --- a/pkg/internal/common/sign_test.go +++ b/pkg/internal/common/sign_test.go @@ -5,6 +5,8 @@ import ( "fmt" "testing" + b64 "encoding/base64" + "github.com/String-xyz/string-api/pkg/model" "github.com/joho/godotenv" "github.com/stretchr/testify/assert" @@ -14,12 +16,17 @@ func TestSignAndValidateString(t *testing.T) { err := godotenv.Load("../../../.env") assert.NoError(t, err) - obj1 := []byte("Your String Here") + encodedMessage := "Your base64 encoded String Here" + + // decode + decoded, err := b64.URLEncoding.DecodeString(encodedMessage) + assert.NoError(t, err) - obj1Signed, err := EVMSign(obj1, true) + // sign + obj1Signed, err := EVMSign(decoded, true) assert.NoError(t, err) fmt.Printf("\nString Signature: %+v\n", obj1Signed) - valid, err := ValidateEVMSignature(obj1Signed, obj1, true) + valid, err := ValidateEVMSignature(obj1Signed, decoded, true) assert.NoError(t, err) assert.Equal(t, true, valid) } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 3c24fd51..e1b825ad 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -22,8 +22,6 @@ type SignablePayload struct { var hexRegex *regexp.Regexp = regexp.MustCompile(`^0x[a-fA-F0-9]{40}$`) -// var walletAuthenticationPrefix string = "" // For testing locally - var walletAuthenticationPrefix string = "Thank you for using String! By signing this message you are:\n\n1) Authorizing String to initiate off-chain transactions on your behalf, including your bank account, credit card, or debit card.\n\n2) Confirming that this wallet is owned by you.\n\nThis request will not trigger any blockchain transaction or cost any gas.\n\nNonce: " type RefreshTokenResponse struct { From 03424df35bddabe434d79b20911cd60bd66329f5 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 14:28:56 -0700 Subject: [PATCH 04/35] move admin api entities into admin api --- pkg/model/entity.go | 67 ++------------------------------------------- 1 file changed, 2 insertions(+), 65 deletions(-) diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 0a53dcd2..bb2fa7f6 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -22,8 +22,8 @@ type User struct { LastName string `json:"lastName" db:"last_name"` } -// See PLATFORM in Migrations 0001 -type Platform_DEPRECATED struct { +// See PLATFORM in Migrations 0001 -- THIS IS DEPRECATED +type Platform struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` @@ -205,66 +205,3 @@ type AuthStrategy struct { func (a AuthStrategy) MarshalBinary() ([]byte, error) { return json.Marshal(a) } - -type Platform struct { - ID string `json:"id,omitempty" db:"id"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - ActivatedAt *time.Time `json:"activatedAt,omitempty" db:"activated_at"` - Name string `json:"name" db:"name"` - Description string `json:"description" db:"description"` - Domains pq.StringArray `json:"domains" db:"domains"` - IPAddresses pq.StringArray `json:"ipAddresses" db:"ip_addresses"` -} - -type Member struct { - ID string `json:"id,omitempty" db:"id"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Email string `json:"email" db:"email"` - Password string `json:"password" db:"password"` -} - -type PlatformMember struct { - PlatformID string `json:"platformId" db:"platform_id"` - MemberID string `json:"memberId" db:"member_id"` -} - -type Role struct { - ID string `json:"id,omitempty" db:"id"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Name string `json:"name" db:"name"` -} - -type MemberRole struct { - MemberID string `json:"memberId" db:"member_id"` - RoleID string `json:"roleId" db:"role_id"` -} - -type Invite struct { - ID string `json:"id,omitempty" db:"id"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - ExpiredAt *time.Time `json:"expiredAt,omitempty" db:"expired_at"` - AcceptedAt *time.Time `json:"acceptedAt,omitempty" db:"accepted_at"` - Email string `json:"email" db:"email"` - InvitedBy string `json:"invitedBy" db:"invited_by"` - PlatformID string `json:"platformId" db:"platform_id"` -} - -type Apikey struct { - ID string `json:"id,omitempty" db:"id"` - CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Data string `json:"data" db:"data"` - Description string `json:"description" db:"description"` - CreatedBy string `json:"createdBy" db:"created_by"` - PlatformID string `json:"platformId" db:"platform_id"` -} From 9ae4940846e4389d15ac4ca3ecae79293143a078 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 15:13:41 -0700 Subject: [PATCH 05/35] deprecate old 'platform' --- .../0001_string-user_platform_asset_network.sql | 10 +++++----- ...platform_device_contact_location_instrument.sql | 2 +- ...atform_device-instrument_tx-leg_transaction.sql | 4 ++-- ...tform-member_role_member-role_invite_apikey.sql | 14 +++++++------- pkg/repository/platform.go | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/migrations/0001_string-user_platform_asset_network.sql b/migrations/0001_string-user_platform_asset_network.sql index 06590b6d..bbd157f5 100644 --- a/migrations/0001_string-user_platform_asset_network.sql +++ b/migrations/0001_string-user_platform_asset_network.sql @@ -43,7 +43,7 @@ EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- -CREATE TABLE platform ( +CREATE TABLE platform_deprecated ( id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -54,9 +54,9 @@ CREATE TABLE platform ( api_key TEXT DEFAULT '', authentication TEXT DEFAULT '' --enum [email, phone, wallet] ); -CREATE OR REPLACE TRIGGER update_platform_updated_at +CREATE OR REPLACE TRIGGER update_platform_deprecated_updated_at BEFORE UPDATE - ON platform + ON platform_deprecated FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); @@ -121,8 +121,8 @@ DROP TABLE IF EXISTS network; ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- -DROP TRIGGER IF EXISTS update_platform_updated_at ON platfom; -DROP TABLE IF EXISTS platform; +DROP TRIGGER IF EXISTS update_platform_deprecated_updated_at ON platform_deprecated; +DROP TABLE IF EXISTS platform_deprecated; ------------------------------------------------------------------------- -- STRING_USER ---------------------------------------------------------- diff --git a/migrations/0002_user-platform_device_contact_location_instrument.sql b/migrations/0002_user-platform_device_contact_location_instrument.sql index 8d7e009b..4820ad57 100644 --- a/migrations/0002_user-platform_device_contact_location_instrument.sql +++ b/migrations/0002_user-platform_device_contact_location_instrument.sql @@ -5,7 +5,7 @@ -- USER_PLATFORM -------------------------------------------------------- CREATE TABLE user_platform ( user_id UUID REFERENCES string_user (id), - platform_id UUID REFERENCES platform (id) + platform_id UUID REFERENCES platform_deprecated (id) ); CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_id, platform_id); diff --git a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql index a981ef0b..cd6e73ab 100644 --- a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql +++ b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql @@ -5,7 +5,7 @@ -- CONTACT_PLATFORM ----------------------------------------------------- CREATE TABLE contact_platform ( contact_id UUID REFERENCES contact (id), - platform_id UUID REFERENCES platform (id) + platform_id UUID REFERENCES platform_deprecated (id) ); CREATE UNIQUE INDEX contact_platform_contact_id_platform_id_idx ON contact_platform(contact_id, platform_id); @@ -67,7 +67,7 @@ CREATE TABLE transaction ( tags JSONB DEFAULT '{}'::JSONB, -- Empty but will be used for Unit21. These are key-val pairs for flagging transactions device_id UUID REFERENCES device (id), -- id that correlates to end-users device in our Device table, we get the data from fingerprint.com -- TODO: Get this with fingerprint integration ip_address TEXT DEFAULT '', -- we get this data from fingerprint.com, whatever is being used at time of transaction - platform_id UUID REFERENCES platform (id), -- id that correlates to CUSTOMER in our Platform table (ie gamefi.xyz) + platform_id UUID REFERENCES platform_deprecated (id), -- id that correlates to CUSTOMER in our Platform table (ie gamefi.xyz) transaction_hash TEXT DEFAULT '', -- EVM/network TX ID after it is generated by executor network_id UUID NOT NULL REFERENCES network (id), -- id that correlates to the Network (Chain) in our Network table network_fee TEXT DEFAULT '', -- The true amount of gas in wei that was used to facilitate the transaction diff --git a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql index a8d85291..daa8dede 100644 --- a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql +++ b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql @@ -12,7 +12,7 @@ CREATE TABLE platform ( name TEXT NOT NULL, description TEXT NOT NULL, domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) - ip_addresses TEXT[] DEFAULT NULL, -- define which API ips can make calls (API-to-API) + ip_addresses TEXT[] DEFAULT NULL -- define which API ips can make calls (API-to-API) ); ------------------------------------------------------------------------- @@ -23,7 +23,7 @@ CREATE TABLE member ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, - password TEXT NOT NULL, -- how do we maintain this? + password TEXT NOT NULL -- how do we maintain this? ); ------------------------------------------------------------------------- @@ -42,14 +42,14 @@ CREATE TABLE role ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - name TEXT NOT NULL, + name TEXT NOT NULL ); ------------------------------------------------------------------------- -- MEMBER_ROLE ----------------------------------------------------- CREATE TABLE member_role ( - member_id UUID REFERENCES member (id) - role_id UUID REFERENCES role (id), + member_id UUID REFERENCES member (id), + role_id UUID REFERENCES role (id) ); CREATE UNIQUE INDEX member_role_member_id_role_id_idx ON member_role(member_id, role_id); @@ -65,7 +65,7 @@ CREATE TABLE invite ( accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, invited_by UUID REFERENCES member (id), - platform_id UUID REFERENCES platform (id), + platform_id UUID REFERENCES platform (id) ); ------------------------------------------------------------------------- @@ -79,7 +79,7 @@ CREATE TABLE apikey ( data TEXT NOT NULL, -- the key itself description TEXT NOT NULL, created_by UUID REFERENCES member (id), - platform_id UUID REFERENCES platform (id), + platform_id UUID REFERENCES platform (id) ); diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index 8313cbee..f4c712e6 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -32,13 +32,13 @@ type platform[T any] struct { } func NewPlatform(db *sqlx.DB) Platform { - return &platform[model.Platform]{base: base[model.Platform]{store: db, table: "platform"}} + return &platform[model.Platform]{base: base[model.Platform]{store: db, table: "platform_deprecated"}} } func (p platform[T]) Create(m model.Platform) (model.Platform, error) { plat := model.Platform{} rows, err := p.store.NamedQuery(` - INSERT INTO platform (type, authentication, api_key, status) + INSERT INTO platform_deprecated (type, authentication, api_key, status) VALUES(:type, :authentication, :api_key, :status) RETURNING *`, m) if err != nil { From 53d4dffa692ff0d56fd260d5b6a96ea1d4cb4172 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 16:06:19 -0700 Subject: [PATCH 06/35] alter platform table instead of dropping it --- ...001_string-user_platform_asset_network.sql | 10 +++--- ...orm_device_contact_location_instrument.sql | 2 +- ...m_device-instrument_tx-leg_transaction.sql | 4 +-- ...-member_role_member-role_invite_apikey.sql | 36 ++++++++++++------- pkg/repository/platform.go | 4 +-- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/migrations/0001_string-user_platform_asset_network.sql b/migrations/0001_string-user_platform_asset_network.sql index bbd157f5..265d424b 100644 --- a/migrations/0001_string-user_platform_asset_network.sql +++ b/migrations/0001_string-user_platform_asset_network.sql @@ -43,7 +43,7 @@ EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- -CREATE TABLE platform_deprecated ( +CREATE TABLE platform ( id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -54,9 +54,9 @@ CREATE TABLE platform_deprecated ( api_key TEXT DEFAULT '', authentication TEXT DEFAULT '' --enum [email, phone, wallet] ); -CREATE OR REPLACE TRIGGER update_platform_deprecated_updated_at +CREATE OR REPLACE TRIGGER update_platform_updated_at BEFORE UPDATE - ON platform_deprecated + ON platform FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); @@ -121,8 +121,8 @@ DROP TABLE IF EXISTS network; ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- -DROP TRIGGER IF EXISTS update_platform_deprecated_updated_at ON platform_deprecated; -DROP TABLE IF EXISTS platform_deprecated; +DROP TRIGGER IF EXISTS update_platform_updated_at ON platform; +DROP TABLE IF EXISTS platform; ------------------------------------------------------------------------- -- STRING_USER ---------------------------------------------------------- diff --git a/migrations/0002_user-platform_device_contact_location_instrument.sql b/migrations/0002_user-platform_device_contact_location_instrument.sql index 4820ad57..8d7e009b 100644 --- a/migrations/0002_user-platform_device_contact_location_instrument.sql +++ b/migrations/0002_user-platform_device_contact_location_instrument.sql @@ -5,7 +5,7 @@ -- USER_PLATFORM -------------------------------------------------------- CREATE TABLE user_platform ( user_id UUID REFERENCES string_user (id), - platform_id UUID REFERENCES platform_deprecated (id) + platform_id UUID REFERENCES platform (id) ); CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_id, platform_id); diff --git a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql index cd6e73ab..a981ef0b 100644 --- a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql +++ b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql @@ -5,7 +5,7 @@ -- CONTACT_PLATFORM ----------------------------------------------------- CREATE TABLE contact_platform ( contact_id UUID REFERENCES contact (id), - platform_id UUID REFERENCES platform_deprecated (id) + platform_id UUID REFERENCES platform (id) ); CREATE UNIQUE INDEX contact_platform_contact_id_platform_id_idx ON contact_platform(contact_id, platform_id); @@ -67,7 +67,7 @@ CREATE TABLE transaction ( tags JSONB DEFAULT '{}'::JSONB, -- Empty but will be used for Unit21. These are key-val pairs for flagging transactions device_id UUID REFERENCES device (id), -- id that correlates to end-users device in our Device table, we get the data from fingerprint.com -- TODO: Get this with fingerprint integration ip_address TEXT DEFAULT '', -- we get this data from fingerprint.com, whatever is being used at time of transaction - platform_id UUID REFERENCES platform_deprecated (id), -- id that correlates to CUSTOMER in our Platform table (ie gamefi.xyz) + platform_id UUID REFERENCES platform (id), -- id that correlates to CUSTOMER in our Platform table (ie gamefi.xyz) transaction_hash TEXT DEFAULT '', -- EVM/network TX ID after it is generated by executor network_id UUID NOT NULL REFERENCES network (id), -- id that correlates to the Network (Chain) in our Network table network_fee TEXT DEFAULT '', -- The true amount of gas in wei that was used to facilitate the transaction diff --git a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql index daa8dede..c78e0678 100644 --- a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql +++ b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql @@ -3,17 +3,17 @@ ------------------------------------------------------------------------- -- PLATFORM ----------------------------------------------------- -CREATE TABLE platform ( - id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users - name TEXT NOT NULL, - description TEXT NOT NULL, - domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) - ip_addresses TEXT[] DEFAULT NULL -- define which API ips can make calls (API-to-API) -); +ALTER TABLE platform + DROP COLUMN IF EXISTS type, + DROP COLUMN IF EXISTS status, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS api_key, + DROP COLUMN IF EXISTS authentication, + ADD COLUMN activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users + ADD COLUMN name TEXT NOT NULL, + ADD COLUMN description TEXT NOT NULL, + ADD COLUMN domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) + ADD COLUMN ip_addresses TEXT[] DEFAULT NULL; -- define which API ips can make calls (API-to-API) ------------------------------------------------------------------------- -- MEMBER ----------------------------------------------------- @@ -87,8 +87,18 @@ CREATE TABLE apikey ( -- +goose Down ------------------------------------------------------------------------- --- CONTACT_PLATFORM ----------------------------------------------------- -DROP TABLE IF EXISTS platform; +-- PLATFORM ----------------------------------------------------- +ALTER TABLE platform + DROP COLUMN IF EXISTS activated_at, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS description, + DROP COLUMN IF EXISTS domains, + DROP COLUMN IF EXISTS ip_addresses + ADD COLUMN type TEXT NOT NULL, -- enum: to be defined at struct level in Go + ADD COLUMN status TEXT NOT NULL, -- enum: to be defined at struct level in Go + ADD COLUMN name TEXT DEFAULT '', + ADD COLUMN api_key TEXT DEFAULT '', + ADD COLUMN authentication TEXT DEFAULT ''; --enum [email, phone, wallet] ------------------------------------------------------------------------- -- MEMBER ----------------------------------------------------- diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index f4c712e6..8313cbee 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -32,13 +32,13 @@ type platform[T any] struct { } func NewPlatform(db *sqlx.DB) Platform { - return &platform[model.Platform]{base: base[model.Platform]{store: db, table: "platform_deprecated"}} + return &platform[model.Platform]{base: base[model.Platform]{store: db, table: "platform"}} } func (p platform[T]) Create(m model.Platform) (model.Platform, error) { plat := model.Platform{} rows, err := p.store.NamedQuery(` - INSERT INTO platform_deprecated (type, authentication, api_key, status) + INSERT INTO platform (type, authentication, api_key, status) VALUES(:type, :authentication, :api_key, :status) RETURNING *`, m) if err != nil { From 4373877b0f01c948fa67d054b334b1a9bcc933e2 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 17:04:41 -0700 Subject: [PATCH 07/35] change tabs to 2 spaces in migrations 5 --- ...rm_member_platform-member_role_member-role_invite_apikey.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql index c78e0678..5c51c85e 100644 --- a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql +++ b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql @@ -2,7 +2,7 @@ -- +goose Up ------------------------------------------------------------------------- --- PLATFORM ----------------------------------------------------- +-- PLATFORM ---------------------------------------------------- ALTER TABLE platform DROP COLUMN IF EXISTS type, DROP COLUMN IF EXISTS status, From 1970863108f8cde1cba5d0f088c060ef32c8e3a7 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 18:06:22 -0700 Subject: [PATCH 08/35] Updated this_to_that naming convention in entities, repos --- api/config.go | 24 +++++++-------- pkg/internal/unit21/entity.go | 8 ++--- pkg/model/entity.go | 6 ++-- pkg/repository/base.go | 24 +++++++-------- pkg/repository/contact_platform.go | 42 -------------------------- pkg/repository/contact_to_platform.go | 42 ++++++++++++++++++++++++++ pkg/repository/user_platform.go | 43 --------------------------- pkg/repository/user_to_platform.go | 43 +++++++++++++++++++++++++++ pkg/service/user.go | 12 ++++---- 9 files changed, 122 insertions(+), 122 deletions(-) delete mode 100644 pkg/repository/contact_platform.go create mode 100644 pkg/repository/contact_to_platform.go delete mode 100644 pkg/repository/user_platform.go create mode 100644 pkg/repository/user_to_platform.go diff --git a/api/config.go b/api/config.go index a783a3fd..22a974b5 100644 --- a/api/config.go +++ b/api/config.go @@ -10,18 +10,18 @@ import ( func NewRepos(config APIConfig) repository.Repositories { // TODO: Make sure all of the repos are initialized here return repository.Repositories{ - Auth: repository.NewAuth(config.Redis, config.DB), - User: repository.NewUser(config.DB), - Contact: repository.NewContact(config.DB), - Instrument: repository.NewInstrument(config.DB), - Device: repository.NewDevice(config.DB), - UserPlatform: repository.NewUserPlatform(config.DB), - Asset: repository.NewAsset(config.DB), - Network: repository.NewNetwork(config.DB), - Platform: repository.NewPlatform(config.DB), - Transaction: repository.NewTransaction(config.DB), - TxLeg: repository.NewTxLeg(config.DB), - Location: repository.NewLocation(config.DB), + Auth: repository.NewAuth(config.Redis, config.DB), + User: repository.NewUser(config.DB), + Contact: repository.NewContact(config.DB), + Instrument: repository.NewInstrument(config.DB), + Device: repository.NewDevice(config.DB), + UserToPlatform: repository.NewUserToPlatform(config.DB), + Asset: repository.NewAsset(config.DB), + Network: repository.NewNetwork(config.DB), + Platform: repository.NewPlatform(config.DB), + Transaction: repository.NewTransaction(config.DB), + TxLeg: repository.NewTxLeg(config.DB), + Location: repository.NewLocation(config.DB), } } diff --git a/pkg/internal/unit21/entity.go b/pkg/internal/unit21/entity.go index 958ba091..3437e593 100644 --- a/pkg/internal/unit21/entity.go +++ b/pkg/internal/unit21/entity.go @@ -17,9 +17,9 @@ type Entity interface { } type EntityRepos struct { - Device repository.Device - Contact repository.Contact - UserPlatform repository.UserPlatform + Device repository.Device + Contact repository.Contact + UserToPlatform repository.UserToPlatform } type entity struct { @@ -176,7 +176,7 @@ func (e entity) getEntityDigitalData(userId string) (deviceData entityDigitalDat } func (e entity) getCustomData(userId string) (customData entityCustomData, err error) { - devices, err := e.repo.UserPlatform.ListByUserId(userId, 100, 0) + devices, err := e.repo.UserToPlatform.ListByUserId(userId, 100, 0) if err != nil { log.Printf("Failed to get user platforms: %s", err) err = common.StringError(err) diff --git a/pkg/model/entity.go b/pkg/model/entity.go index bb2fa7f6..96382a7f 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -65,7 +65,7 @@ type Asset struct { } // See USER_PLATFORM in Migrations 0002 -type UserPlatform struct { +type UserToPlatform struct { UserID string `json:"userId" db:"user_id"` PlatformID string `json:"platformId" db:"platform_id"` } @@ -135,13 +135,13 @@ type Instrument struct { } // See CONTACT_PLATFORM in Migrations 0003 -type ContactPlatform struct { +type ContactToPlatform struct { ContactID string `json:"contactId" db:"contact_id"` PlatformID string `json:"platformId" db:"platform_id"` } // See DEVICE_INSTRUMENT in Migrations 0003 -type DeviceInstrument struct { +type DeviceToInstrument struct { DeviceID string `json:"deviceId" db:"device_id"` InstrumentID string `json:"instrumentId" db:"instrument_id"` } diff --git a/pkg/repository/base.go b/pkg/repository/base.go index 87b5ccb9..8f78d12a 100644 --- a/pkg/repository/base.go +++ b/pkg/repository/base.go @@ -15,18 +15,18 @@ import ( var ErrNotFound = errors.New("not found") type Repositories struct { - Auth AuthStrategy - User User - Contact Contact - Instrument Instrument - Device Device - UserPlatform UserPlatform - Asset Asset - Network Network - Platform Platform - Transaction Transaction - TxLeg TxLeg - Location Location + Auth AuthStrategy + User User + Contact Contact + Instrument Instrument + Device Device + UserToPlatform UserToPlatform + Asset Asset + Network Network + Platform Platform + Transaction Transaction + TxLeg TxLeg + Location Location } type Queryable interface { diff --git a/pkg/repository/contact_platform.go b/pkg/repository/contact_platform.go deleted file mode 100644 index 8f4a29d0..00000000 --- a/pkg/repository/contact_platform.go +++ /dev/null @@ -1,42 +0,0 @@ -package repository - -import ( - "github.com/String-xyz/string-api/pkg/internal/common" - "github.com/String-xyz/string-api/pkg/model" - "github.com/jmoiron/sqlx" -) - -type ContactPlatform interface { - Transactable - Readable - Create(model.ContactPlatform) (model.ContactPlatform, error) - GetById(ID string) (model.ContactPlatform, error) - List(limit int, offset int) ([]model.ContactPlatform, error) - Update(ID string, updates any) error -} - -type contactPlatform[T any] struct { - base[T] -} - -func NewContactPlatform(db *sqlx.DB) ContactPlatform { - return &contactPlatform[model.ContactPlatform]{base: base[model.ContactPlatform]{store: db, table: "contact_platform"}} -} - -func (u contactPlatform[T]) Create(insert model.ContactPlatform) (model.ContactPlatform, error) { - m := model.ContactPlatform{} - rows, err := u.store.NamedQuery(` - INSERT INTO contact_platform (contact_id, platform_id) - VALUES(:contact_id, :platform_id) RETURNING *`, insert) - if err != nil { - return m, common.StringError(err) - } - for rows.Next() { - err = rows.StructScan(&m) - if err != nil { - return m, common.StringError(err) - } - } - defer rows.Close() - return m, nil -} diff --git a/pkg/repository/contact_to_platform.go b/pkg/repository/contact_to_platform.go new file mode 100644 index 00000000..ca0d1543 --- /dev/null +++ b/pkg/repository/contact_to_platform.go @@ -0,0 +1,42 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type ContactToPlatform interface { + Transactable + Readable + Create(model.ContactToPlatform) (model.ContactToPlatform, error) + GetById(ID string) (model.ContactToPlatform, error) + List(limit int, offset int) ([]model.ContactToPlatform, error) + Update(ID string, updates any) error +} + +type contactToPlatform[T any] struct { + base[T] +} + +func NewContactPlatform(db *sqlx.DB) ContactToPlatform { + return &contactToPlatform[model.ContactToPlatform]{base: base[model.ContactToPlatform]{store: db, table: "contact_to_platform"}} +} + +func (u contactToPlatform[T]) Create(insert model.ContactToPlatform) (model.ContactToPlatform, error) { + m := model.ContactToPlatform{} + rows, err := u.store.NamedQuery(` + INSERT INTO contact_to_platform (contact_id, platform_id) + VALUES(:contact_id, :platform_id) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + defer rows.Close() + return m, nil +} diff --git a/pkg/repository/user_platform.go b/pkg/repository/user_platform.go deleted file mode 100644 index b098150d..00000000 --- a/pkg/repository/user_platform.go +++ /dev/null @@ -1,43 +0,0 @@ -package repository - -import ( - "github.com/String-xyz/string-api/pkg/internal/common" - "github.com/String-xyz/string-api/pkg/model" - "github.com/jmoiron/sqlx" -) - -type UserPlatform interface { - Transactable - Readable - Create(model.UserPlatform) (model.UserPlatform, error) - GetById(ID string) (model.UserPlatform, error) - List(limit int, offset int) ([]model.UserPlatform, error) - ListByUserId(userID string, imit int, offset int) ([]model.UserPlatform, error) - Update(ID string, updates any) error -} - -type userPlatform[T any] struct { - base[T] -} - -func NewUserPlatform(db *sqlx.DB) UserPlatform { - return &userPlatform[model.UserPlatform]{base: base[model.UserPlatform]{store: db, table: "user_platform"}} -} - -func (u userPlatform[T]) Create(insert model.UserPlatform) (model.UserPlatform, error) { - m := model.UserPlatform{} - rows, err := u.store.NamedQuery(` - INSERT INTO user_platform (user_id, platform_id) - VALUES(:user_id, :platform_id) RETURNING *`, insert) - if err != nil { - return m, common.StringError(err) - } - for rows.Next() { - err = rows.StructScan(&m) - if err != nil { - return m, common.StringError(err) - } - } - defer rows.Close() - return m, nil -} diff --git a/pkg/repository/user_to_platform.go b/pkg/repository/user_to_platform.go new file mode 100644 index 00000000..c3fc066b --- /dev/null +++ b/pkg/repository/user_to_platform.go @@ -0,0 +1,43 @@ +package repository + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/jmoiron/sqlx" +) + +type UserToPlatform interface { + Transactable + Readable + Create(model.UserToPlatform) (model.UserToPlatform, error) + GetById(ID string) (model.UserToPlatform, error) + List(limit int, offset int) ([]model.UserToPlatform, error) + ListByUserId(userID string, imit int, offset int) ([]model.UserToPlatform, error) + Update(ID string, updates any) error +} + +type userToPlatform[T any] struct { + base[T] +} + +func NewUserToPlatform(db *sqlx.DB) UserToPlatform { + return &userToPlatform[model.UserToPlatform]{base: base[model.UserToPlatform]{store: db, table: "user_to_platform"}} +} + +func (u userToPlatform[T]) Create(insert model.UserToPlatform) (model.UserToPlatform, error) { + m := model.UserToPlatform{} + rows, err := u.store.NamedQuery(` + INSERT INTO user_to_platform (user_id, platform_id) + VALUES(:user_id, :platform_id) RETURNING *`, insert) + if err != nil { + return m, common.StringError(err) + } + for rows.Next() { + err = rows.StructScan(&m) + if err != nil { + return m, common.StringError(err) + } + } + defer rows.Close() + return m, nil +} diff --git a/pkg/service/user.go b/pkg/service/user.go index 13d485f9..f30e53e1 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -173,9 +173,9 @@ func (u user) Update(userID string, request UserUpdates) (model.User, error) { func (u user) createUnit21Entity(user model.User) { // Createing a User Entity in Unit21 u21Repo := unit21.EntityRepos{ - Device: u.repos.Device, - Contact: u.repos.Contact, - UserPlatform: u.repos.UserPlatform, + Device: u.repos.Device, + Contact: u.repos.Contact, + UserToPlatform: u.repos.UserToPlatform, } u21Entity := unit21.NewEntity(u21Repo) // TODO: Make it an injected dependency @@ -188,9 +188,9 @@ func (u user) createUnit21Entity(user model.User) { func (u user) updateUnit21Entity(user model.User) { // Createing a User Entity in Unit21 u21Repo := unit21.EntityRepos{ - Device: u.repos.Device, - Contact: u.repos.Contact, - UserPlatform: u.repos.UserPlatform, + Device: u.repos.Device, + Contact: u.repos.Contact, + UserToPlatform: u.repos.UserToPlatform, } u21Entity := unit21.NewEntity(u21Repo) From 6f7e9249cb00a1399a94b22a3e2eb649dc1c8a2f Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 26 Jan 2023 19:08:24 -0600 Subject: [PATCH 09/35] fixed some spacing, renamed many-to-many relationships as table-to-table --- ...001_string-user_platform_asset_network.sql | 30 ++--- ...orm_device_contact_location_instrument.sql | 24 ++-- ...m_device-instrument_tx-leg_transaction.sql | 12 +- migrations/0004_auth_key.sql | 6 +- ...-member_role_member-role_invite_apikey.sql | 125 ------------------ ...platform_device-to-instrument_platform.sql | 94 +++++++++++++ ...le_member-to-role_member-invite_apikey.sql | 97 ++++++++++++++ 7 files changed, 227 insertions(+), 161 deletions(-) delete mode 100644 migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql create mode 100644 migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql create mode 100644 migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql diff --git a/migrations/0001_string-user_platform_asset_network.sql b/migrations/0001_string-user_platform_asset_network.sql index 265d424b..d816e3c3 100644 --- a/migrations/0001_string-user_platform_asset_network.sql +++ b/migrations/0001_string-user_platform_asset_network.sql @@ -10,11 +10,11 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; ------------------------------------------------------------------------- -- +goose StatementBegin CREATE OR REPLACE FUNCTION update_updated_at_column() - RETURNS TRIGGER AS + RETURNS TRIGGER AS $$ BEGIN - NEW.updated_at = now(); - RETURN NEW; + NEW.updated_at = now(); + RETURN NEW; END; $$ language 'plpgsql'; -- +goose StatementEnd @@ -36,9 +36,9 @@ CREATE TABLE string_user ( ); CREATE OR REPLACE TRIGGER update_string_user_updated_at - BEFORE UPDATE - ON string_user - FOR EACH ROW + BEFORE UPDATE + ON string_user + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -55,9 +55,9 @@ CREATE TABLE platform ( authentication TEXT DEFAULT '' --enum [email, phone, wallet] ); CREATE OR REPLACE TRIGGER update_platform_updated_at - BEFORE UPDATE - ON platform - FOR EACH ROW + BEFORE UPDATE + ON platform + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -76,9 +76,9 @@ CREATE TABLE network ( explorer_url TEXT DEFAULT '' -- The Block Explorer URL used to view transactions and entities in the browser ); CREATE OR REPLACE TRIGGER update_network_updated_at - BEFORE UPDATE - ON network - FOR EACH ROW + BEFORE UPDATE + ON network + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -97,9 +97,9 @@ CREATE TABLE asset ( -- We will write sql commands to add/update these in bulk. ); CREATE OR REPLACE TRIGGER update_asset_updated_at - BEFORE UPDATE - ON asset - FOR EACH ROW + BEFORE UPDATE + ON asset + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE INDEX network_gas_token_id_fk ON network (gas_token_id); diff --git a/migrations/0002_user-platform_device_contact_location_instrument.sql b/migrations/0002_user-platform_device_contact_location_instrument.sql index 8d7e009b..4315b060 100644 --- a/migrations/0002_user-platform_device_contact_location_instrument.sql +++ b/migrations/0002_user-platform_device_contact_location_instrument.sql @@ -27,9 +27,9 @@ CREATE TABLE device ( ); CREATE OR REPLACE TRIGGER update_device_updated_at - BEFORE UPDATE - ON device - FOR EACH ROW + BEFORE UPDATE + ON device + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE UNIQUE INDEX device_fingerprint_id_idx ON device(fingerprint, user_id); @@ -49,9 +49,9 @@ CREATE TABLE contact ( ); CREATE OR REPLACE TRIGGER update_contact_updated_at - BEFORE UPDATE - ON contact - FOR EACH ROW + BEFORE UPDATE + ON contact + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -74,9 +74,9 @@ CREATE TABLE location ( ); CREATE OR REPLACE TRIGGER update_location_updated_at - BEFORE UPDATE - ON location - FOR EACH ROW + BEFORE UPDATE + ON location + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -97,9 +97,9 @@ CREATE TABLE instrument ( ); CREATE OR REPLACE TRIGGER update_instrument_updated_at - BEFORE UPDATE - ON instrument - FOR EACH ROW + BEFORE UPDATE + ON instrument + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- diff --git a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql index a981ef0b..99d8a319 100644 --- a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql +++ b/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql @@ -50,9 +50,9 @@ CREATE TABLE tx_leg ( ); CREATE OR REPLACE TRIGGER update_tx_leg_updated_at - BEFORE UPDATE - ON tx_leg - FOR EACH ROW + BEFORE UPDATE + ON tx_leg + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- @@ -84,9 +84,9 @@ CREATE TABLE transaction ( ); CREATE OR REPLACE TRIGGER update_transaction_updated_at - BEFORE UPDATE - ON transaction - FOR EACH ROW + BEFORE UPDATE + ON transaction + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); ------------------------------------------------------------------------- diff --git a/migrations/0004_auth_key.sql b/migrations/0004_auth_key.sql index 40bfa569..9295d939 100644 --- a/migrations/0004_auth_key.sql +++ b/migrations/0004_auth_key.sql @@ -11,9 +11,9 @@ CREATE TABLE auth_strategy ( ); CREATE OR REPLACE TRIGGER update_auth_strategy_updated_at - BEFORE UPDATE - ON auth_strategy - FOR EACH ROW + BEFORE UPDATE + ON auth_strategy + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); CREATE INDEX auth_strategy_status_idx ON auth_strategy(status); diff --git a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql b/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql deleted file mode 100644 index 5c51c85e..00000000 --- a/migrations/0005_platform_member_platform-member_role_member-role_invite_apikey.sql +++ /dev/null @@ -1,125 +0,0 @@ -------------------------------------------------------------------------- --- +goose Up - -------------------------------------------------------------------------- --- PLATFORM ---------------------------------------------------- -ALTER TABLE platform - DROP COLUMN IF EXISTS type, - DROP COLUMN IF EXISTS status, - DROP COLUMN IF EXISTS name, - DROP COLUMN IF EXISTS api_key, - DROP COLUMN IF EXISTS authentication, - ADD COLUMN activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users - ADD COLUMN name TEXT NOT NULL, - ADD COLUMN description TEXT NOT NULL, - ADD COLUMN domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) - ADD COLUMN ip_addresses TEXT[] DEFAULT NULL; -- define which API ips can make calls (API-to-API) - -------------------------------------------------------------------------- --- MEMBER ----------------------------------------------------- -CREATE TABLE member ( - id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - email TEXT NOT NULL, - password TEXT NOT NULL -- how do we maintain this? -); - -------------------------------------------------------------------------- --- PLATFORM_MEMBER ----------------------------------------------------- -CREATE TABLE platform_member ( - platform_id UUID REFERENCES platform (id), - member_id UUID REFERENCES member (id) -); - -CREATE UNIQUE INDEX platform_member_platform_id_member_id_idx ON platform_member(platform_id, member_id); - -------------------------------------------------------------------------- --- ROLE ----------------------------------------------------- -CREATE TABLE role ( - id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - name TEXT NOT NULL -); - -------------------------------------------------------------------------- --- MEMBER_ROLE ----------------------------------------------------- -CREATE TABLE member_role ( - member_id UUID REFERENCES member (id), - role_id UUID REFERENCES role (id) -); - -CREATE UNIQUE INDEX member_role_member_id_role_id_idx ON member_role(member_id, role_id); - -------------------------------------------------------------------------- --- INVITE ----------------------------------------------------- -CREATE TABLE invite ( - id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - expired_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - email TEXT NOT NULL, - invited_by UUID REFERENCES member (id), - platform_id UUID REFERENCES platform (id) -); - -------------------------------------------------------------------------- --- APIKEY ----------------------------------------------------- -CREATE TABLE apikey ( - id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - type TEXT NOT NULL, -- [public,private] for now all public? - data TEXT NOT NULL, -- the key itself - description TEXT NOT NULL, - created_by UUID REFERENCES member (id), - platform_id UUID REFERENCES platform (id) -); - - -------------------------------------------------------------------------- --- +goose Down - -------------------------------------------------------------------------- --- PLATFORM ----------------------------------------------------- -ALTER TABLE platform - DROP COLUMN IF EXISTS activated_at, - DROP COLUMN IF EXISTS name, - DROP COLUMN IF EXISTS description, - DROP COLUMN IF EXISTS domains, - DROP COLUMN IF EXISTS ip_addresses - ADD COLUMN type TEXT NOT NULL, -- enum: to be defined at struct level in Go - ADD COLUMN status TEXT NOT NULL, -- enum: to be defined at struct level in Go - ADD COLUMN name TEXT DEFAULT '', - ADD COLUMN api_key TEXT DEFAULT '', - ADD COLUMN authentication TEXT DEFAULT ''; --enum [email, phone, wallet] - -------------------------------------------------------------------------- --- MEMBER ----------------------------------------------------- -DROP TABLE IF EXISTS member; - -------------------------------------------------------------------------- --- PLATFORM_MEMBER ----------------------------------------------------- -DROP TABLE IF EXISTS platform_member; - -------------------------------------------------------------------------- --- ROLE ----------------------------------------------------- -DROP TABLE IF EXISTS role; - -------------------------------------------------------------------------- --- MEMBER_ROLE ----------------------------------------------------- -DROP TABLE IF EXISTS member_role; - -------------------------------------------------------------------------- --- INVITE ----------------------------------------------------- -DROP TABLE IF EXISTS invite; - -------------------------------------------------------------------------- --- APIKEY ----------------------------------------------------- -DROP TABLE IF EXISTS apikey; \ No newline at end of file diff --git a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql new file mode 100644 index 00000000..f3626493 --- /dev/null +++ b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql @@ -0,0 +1,94 @@ +------------------------------------------------------------------------- +-- +goose Up + +------------------------------------------------------------------------- +-- USER_TO_PLATFORM ----------------------------------------------------- +ALTER TABLE user_platform + RENAME TO user_to_platform; + +DROP INDEX user_platform_user_id_platform_id_idx IF EXISTS; + +CREATE UNIQUE INDEX user_to_platform_user_id_platform_id_idx ON user_to_platform(user_id, platform_id); + + +------------------------------------------------------------------------- +-- CONTACT_TO_PLATFORM -------------------------------------------------- +ALTER TABLE contact_platform + RENAME TO contact_to_platform; + +DROP INDEX contact_platform_contact_id_platform_id_idx IF EXISTS; + +CREATE UNIQUE INDEX contact_to_platform_contact_id_platform_id_idx ON contact_to_platform(contact_id, platform_id); + + +------------------------------------------------------------------------- +-- DEVICE_TO_INSTRUMENT ------------------------------------------------- +ALTER TABLE device_instrument + RENAME TO device_to_instrument; + +DROP INDEX device_instrument_device_id_instrument_id_idx IF EXISTS; + +CREATE UNIQUE INDEX device_to_instrument_device_id_instrument_id_idx ON device_to_instrument(device_id, instrument_id); + + +------------------------------------------------------------------------- +-- PLATFORM ------------------------------------------------------------- +ALTER TABLE platform + DROP COLUMN IF EXISTS type, + DROP COLUMN IF EXISTS status, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS api_key, + DROP COLUMN IF EXISTS authentication, + ADD COLUMN activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users + ADD COLUMN name TEXT NOT NULL, + ADD COLUMN description TEXT NOT NULL, + ADD COLUMN domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) + ADD COLUMN ip_addresses TEXT[] DEFAULT NULL; -- define which API ips can make calls (API-to-API) + + +------------------------------------------------------------------------- +-- +goose Down + +------------------------------------------------------------------------- +-- PLATFORM ------------------------------------------------------------- +ALTER TABLE platform + DROP COLUMN IF EXISTS activated_at, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS description, + DROP COLUMN IF EXISTS domains, + DROP COLUMN IF EXISTS ip_addresses + ADD COLUMN type TEXT NOT NULL, -- enum: to be defined at struct level in Go + ADD COLUMN status TEXT NOT NULL, -- enum: to be defined at struct level in Go + ADD COLUMN name TEXT DEFAULT '', + ADD COLUMN api_key TEXT DEFAULT '', + ADD COLUMN authentication TEXT DEFAULT ''; --enum [email, phone, wallet] + + +------------------------------------------------------------------------- +-- USER_PLATFORM ----------------------------------------------------- +ALTER TABLE user_to_platform + RENAME TO user_platform; + +DROP INDEX user_to_platform_user_id_platform_id_idx IF EXISTS; + +CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_id, platform_id); + + +------------------------------------------------------------------------- +-- CONTACT_PLATFORM -------------------------------------------------- +ALTER TABLE contact_to_platform + RENAME TO contact_platform; + +DROP INDEX contact_to_platform_contact_id_platform_id_idx IF EXISTS; + +CREATE UNIQUE INDEX contact_platform_contact_id_platform_id_idx ON contact_platform(contact_id, platform_id); + + +------------------------------------------------------------------------- +-- DEVICE_INSTRUMENT ------------------------------------------------- +ALTER TABLE device_to_instrument + RENAME TO device_instrument; + +DROP INDEX device_to_instrument_device_id_instrument_id_idx IF EXISTS; + +CREATE UNIQUE INDEX device_instrument_device_id_instrument_id_idx ON device_instrument(device_id, instrument_id); \ No newline at end of file diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql new file mode 100644 index 00000000..ca8fc054 --- /dev/null +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -0,0 +1,97 @@ +------------------------------------------------------------------------- +-- +goose Up + +------------------------------------------------------------------------- +-- PLATFORM_MEMBER ------------------------------------------------------ +CREATE TABLE platform_member ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + email TEXT NOT NULL, + password TEXT NOT NULL -- how do we maintain this? +); + +------------------------------------------------------------------------- +-- PLATFORM_MEMBER ------------------------------------------------------ +CREATE TABLE member_to_platform ( + member_id UUID REFERENCES platform_member (id), + platform_id UUID REFERENCES platform (id) +); + +CREATE UNIQUE INDEX member_to_platform_platform_id_member_id_idx ON member_to_platform(platform_id, member_id); + +------------------------------------------------------------------------- +-- MEMBER_ROLE ---------------------------------------------------------- +CREATE TABLE member_role ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + name TEXT NOT NULL +); + +------------------------------------------------------------------------- +-- MEMBER_TO_ROLE ------------------------------------------------------- +CREATE TABLE member_to_role ( + member_id UUID REFERENCES platform_member (id), + role_id UUID REFERENCES role (id) +); + +CREATE UNIQUE INDEX member_to_role_member_id_role_id_idx ON member_role(member_id, role_id); + +------------------------------------------------------------------------- +-- MEMBER_INVITE -------------------------------------------------------- +CREATE TABLE member_invite ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + expired_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + email TEXT NOT NULL, + invited_by UUID REFERENCES platform_member (id), + platform_id UUID REFERENCES platform (id) +); + +------------------------------------------------------------------------- +-- APIKEY --------------------------------------------------------------- +CREATE TABLE apikey ( + id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + type TEXT NOT NULL, -- [public,private] for now all public? + data TEXT NOT NULL, -- the key itself + description TEXT NOT NULL, + created_by UUID REFERENCES platform_member (id), + platform_id UUID REFERENCES platform (id) +); + + +------------------------------------------------------------------------- +-- +goose Down + +------------------------------------------------------------------------- +-- PLATFORM_MEMBER ------------------------------------------------------ +DROP TABLE IF EXISTS platform_member; + +------------------------------------------------------------------------- +-- PLATFORM_TO_MEMBER --------------------------------------------------- +DROP TABLE IF EXISTS member_to_platform; + +------------------------------------------------------------------------- +-- MEMBER_ROLE ---------------------------------------------------------- +DROP TABLE IF EXISTS member_role; + +------------------------------------------------------------------------- +-- MEMBER_TO_ROLE ------------------------------------------------------- +DROP TABLE IF EXISTS member_to_role; + +------------------------------------------------------------------------- +-- MEMBER_INVITE -------------------------------------------------------- +DROP TABLE IF EXISTS member_invite; + +------------------------------------------------------------------------- +-- APIKEY --------------------------------------------------------------- +DROP TABLE IF EXISTS apikey; \ No newline at end of file From f63b69b7340877873b029b268ae9b05f0c1fe0c6 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 26 Jan 2023 19:19:38 -0600 Subject: [PATCH 10/35] fix order, IF EXISTS, and missing commas --- ...platform_device-to-instrument_platform.sql | 14 ++++----- ...le_member-to-role_member-invite_apikey.sql | 29 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql index f3626493..0efdc4bc 100644 --- a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql +++ b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql @@ -6,7 +6,7 @@ ALTER TABLE user_platform RENAME TO user_to_platform; -DROP INDEX user_platform_user_id_platform_id_idx IF EXISTS; +DROP INDEX IF EXISTS user_platform_user_id_platform_id_idx; CREATE UNIQUE INDEX user_to_platform_user_id_platform_id_idx ON user_to_platform(user_id, platform_id); @@ -16,7 +16,7 @@ CREATE UNIQUE INDEX user_to_platform_user_id_platform_id_idx ON user_to_platform ALTER TABLE contact_platform RENAME TO contact_to_platform; -DROP INDEX contact_platform_contact_id_platform_id_idx IF EXISTS; +DROP INDEX IF EXISTS contact_platform_contact_id_platform_id_idx; CREATE UNIQUE INDEX contact_to_platform_contact_id_platform_id_idx ON contact_to_platform(contact_id, platform_id); @@ -26,7 +26,7 @@ CREATE UNIQUE INDEX contact_to_platform_contact_id_platform_id_idx ON contact_to ALTER TABLE device_instrument RENAME TO device_to_instrument; -DROP INDEX device_instrument_device_id_instrument_id_idx IF EXISTS; +DROP INDEX IF EXISTS device_instrument_device_id_instrument_id_idx; CREATE UNIQUE INDEX device_to_instrument_device_id_instrument_id_idx ON device_to_instrument(device_id, instrument_id); @@ -56,7 +56,7 @@ ALTER TABLE platform DROP COLUMN IF EXISTS name, DROP COLUMN IF EXISTS description, DROP COLUMN IF EXISTS domains, - DROP COLUMN IF EXISTS ip_addresses + DROP COLUMN IF EXISTS ip_addresses, ADD COLUMN type TEXT NOT NULL, -- enum: to be defined at struct level in Go ADD COLUMN status TEXT NOT NULL, -- enum: to be defined at struct level in Go ADD COLUMN name TEXT DEFAULT '', @@ -69,7 +69,7 @@ ALTER TABLE platform ALTER TABLE user_to_platform RENAME TO user_platform; -DROP INDEX user_to_platform_user_id_platform_id_idx IF EXISTS; +DROP INDEX IF EXISTS user_to_platform_user_id_platform_id_idx; CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_id, platform_id); @@ -79,7 +79,7 @@ CREATE UNIQUE INDEX user_platform_user_id_platform_id_idx ON user_platform(user_ ALTER TABLE contact_to_platform RENAME TO contact_platform; -DROP INDEX contact_to_platform_contact_id_platform_id_idx IF EXISTS; +DROP INDEX IF EXISTS contact_to_platform_contact_id_platform_id_idx; CREATE UNIQUE INDEX contact_platform_contact_id_platform_id_idx ON contact_platform(contact_id, platform_id); @@ -89,6 +89,6 @@ CREATE UNIQUE INDEX contact_platform_contact_id_platform_id_idx ON contact_platf ALTER TABLE device_to_instrument RENAME TO device_instrument; -DROP INDEX device_to_instrument_device_id_instrument_id_idx IF EXISTS; +DROP INDEX IF EXISTS device_to_instrument_device_id_instrument_id_idx; CREATE UNIQUE INDEX device_instrument_device_id_instrument_id_idx ON device_instrument(device_id, instrument_id); \ No newline at end of file diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index ca8fc054..43f665d9 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -35,10 +35,10 @@ CREATE TABLE member_role ( -- MEMBER_TO_ROLE ------------------------------------------------------- CREATE TABLE member_to_role ( member_id UUID REFERENCES platform_member (id), - role_id UUID REFERENCES role (id) + role_id UUID REFERENCES member_role (id) ); -CREATE UNIQUE INDEX member_to_role_member_id_role_id_idx ON member_role(member_id, role_id); +CREATE UNIQUE INDEX member_to_role_member_id_role_id_idx ON member_to_role(member_id, role_id); ------------------------------------------------------------------------- -- MEMBER_INVITE -------------------------------------------------------- @@ -73,25 +73,28 @@ CREATE TABLE apikey ( -- +goose Down ------------------------------------------------------------------------- --- PLATFORM_MEMBER ------------------------------------------------------ -DROP TABLE IF EXISTS platform_member; +-- APIKEY --------------------------------------------------------------- +DROP TABLE IF EXISTS apikey; ------------------------------------------------------------------------- --- PLATFORM_TO_MEMBER --------------------------------------------------- -DROP TABLE IF EXISTS member_to_platform; +-- MEMBER_INVITE -------------------------------------------------------- +DROP TABLE IF EXISTS member_invite; + +------------------------------------------------------------------------- +-- MEMBER_TO_ROLE ------------------------------------------------------- +DROP TABLE IF EXISTS member_to_role; ------------------------------------------------------------------------- -- MEMBER_ROLE ---------------------------------------------------------- DROP TABLE IF EXISTS member_role; ------------------------------------------------------------------------- --- MEMBER_TO_ROLE ------------------------------------------------------- -DROP TABLE IF EXISTS member_to_role; +-- PLATFORM_TO_MEMBER --------------------------------------------------- +DROP TABLE IF EXISTS member_to_platform; ------------------------------------------------------------------------- --- MEMBER_INVITE -------------------------------------------------------- -DROP TABLE IF EXISTS member_invite; +-- PLATFORM_MEMBER ------------------------------------------------------ +DROP TABLE IF EXISTS platform_member; + + -------------------------------------------------------------------------- --- APIKEY --------------------------------------------------------------- -DROP TABLE IF EXISTS apikey; \ No newline at end of file From 58e7788005bd6a04cb68a58df8fb405eca616fff Mon Sep 17 00:00:00 2001 From: Ocasta Date: Thu, 26 Jan 2023 19:33:38 -0600 Subject: [PATCH 11/35] fix race condition in docker --- docker-compose.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 49edbe1b..50239e4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,10 @@ services: ports: - 5555:5555 depends_on: - - db - - redis + redis: + condition: service_started + db: + condition: service_healthy volumes: - ./:/string_api db: @@ -22,6 +24,11 @@ services: - '5432:5432' volumes: - db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 redis: image: redis:7.0-alpine restart: always From 0b114241622a9422ef1755ff61b822bc2105d9be Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 26 Jan 2023 19:05:23 -0700 Subject: [PATCH 12/35] fixed some things and broke some other things --- docker-compose.yml | 2 +- pkg/model/entity.go | 20 ++++++++++---------- pkg/repository/platform.go | 28 +++++++++++++--------------- pkg/service/platform.go | 15 ++++++++------- scripts/data_seeding.go | 5 +++-- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 50239e4d..7108b7bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: volumes: - db:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "pg_isready -U string_db"] interval: 5s timeout: 5s retries: 5 diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 96382a7f..d8a8e54f 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -22,17 +22,17 @@ type User struct { LastName string `json:"lastName" db:"last_name"` } -// See PLATFORM in Migrations 0001 -- THIS IS DEPRECATED +// See PLATFORM in Migrations 0005 type Platform struct { - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"createdAt" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Name string `json:"name" db:"name"` - ApiKey string `json:"apiKey" db:"api_key"` - Authentication AuthType `json:"authentication" db:"authentication"` + ID string `json:"id,omitempty" db:"id"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` + ActivatedAt *time.Time `json:"activatedAt,omitempty" db:"activated_at"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Domains pq.StringArray `json:"domains" db:"domains"` + IPAddresses pq.StringArray `json:"ipAddresses" db:"ip_addresses"` } // See NETWORK in Migrations 0001 diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index 8313cbee..f932da80 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -1,8 +1,6 @@ package repository import ( - "database/sql" - "fmt" "time" "github.com/String-xyz/string-api/pkg/internal/common" @@ -24,7 +22,7 @@ type Platform interface { GetById(ID string) (model.Platform, error) List(limit int, offset int) ([]model.Platform, error) Update(ID string, updates any) error - GetByApiKey(key string) (model.Platform, error) + // GetByApiKey(key string) (model.Platform, error) } type platform[T any] struct { @@ -38,8 +36,8 @@ func NewPlatform(db *sqlx.DB) Platform { func (p platform[T]) Create(m model.Platform) (model.Platform, error) { plat := model.Platform{} rows, err := p.store.NamedQuery(` - INSERT INTO platform (type, authentication, api_key, status) - VALUES(:type, :authentication, :api_key, :status) RETURNING *`, m) + INSERT INTO platform (name, description) + VALUES(:name, :description) RETURNING *`, m) if err != nil { return plat, common.StringError(err) @@ -55,13 +53,13 @@ func (p platform[T]) Create(m model.Platform) (model.Platform, error) { return plat, nil } -func (p platform[T]) GetByApiKey(key string) (model.Platform, error) { - m := model.Platform{} - err := p.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE api_key = $1", p.table), key) - if err != nil && err == sql.ErrNoRows { - return m, common.StringError(ErrNotFound) - } else if err != nil { - return m, common.StringError(err) - } - return m, nil -} +// func (p platform[T]) GetByApiKey(key string) (model.Platform, error) { +// m := model.Platform{} +// err := p.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE api_key = $1", p.table), key) +// if err != nil && err == sql.ErrNoRows { +// return m, common.StringError(ErrNotFound) +// } else if err != nil { +// return m, common.StringError(err) +// } +// return m, nil +// } diff --git a/pkg/service/platform.go b/pkg/service/platform.go index 71999271..7e456f94 100644 --- a/pkg/service/platform.go +++ b/pkg/service/platform.go @@ -23,12 +23,13 @@ func NewPlatform(repos repository.Repositories) Platform { func (a platform) Create(c CreatePlatform) (model.Platform, error) { uuiKey := "str." + uuidWithoutHyphens() hashed := common.ToSha256(uuiKey) - m := model.Platform{ - Type: c.Type, - Authentication: c.Authentication, - ApiKey: hashed, - Status: "pending", - } + // m := model.Platform{ + // Type: c.Type, + // Authentication: c.Authentication, + // ApiKey: hashed, + // Status: "pending", + // } + m := model.Platform{} plat, err := a.repos.Platform.Create(m) if err != nil { @@ -37,7 +38,7 @@ func (a platform) Create(c CreatePlatform) (model.Platform, error) { _, err = a.repos.Auth.CreateAPIKey(plat.ID, c.Authentication, hashed, false) pt := &plat - pt.ApiKey = uuiKey + // pt.ApiKey = uuiKey if err != nil { return *pt, common.StringError(err) } diff --git a/scripts/data_seeding.go b/scripts/data_seeding.go index 805bfdf5..2dd9a52d 100644 --- a/scripts/data_seeding.go +++ b/scripts/data_seeding.go @@ -186,7 +186,8 @@ func DataSeeding() { // Platforms, placeholder /*platformDeveloper*/ - placeholderPlatform, err := repos.Platform.Create(model.Platform{Type: "Game", Status: "Verified", Name: "Nintendo", ApiKey: "Internal", Authentication: "Email"}) + placeholderPlatform, err := repos.Platform.Create(model.Platform{Name: "Nintendo", Description: "Fun"}) + if err != nil { panic(err) } @@ -377,7 +378,7 @@ func MockSeeding() { // Platforms, placeholder /*platformDeveloper*/ - placeholderPlatform, err := repos.Platform.Create(model.Platform{Type: "Game", Status: "Verified", Name: "Nintendo", ApiKey: "Internal", Authentication: "Email"}) + placeholderPlatform, err := repos.Platform.Create(model.Platform{Name: "Nintendo", Description: "Fun"}) if err != nil { panic(err) } From 4d8cef4e53121d9906e2271e212e6cd803f80dab Mon Sep 17 00:00:00 2001 From: Ocasta Date: Fri, 27 Jan 2023 12:36:26 -0600 Subject: [PATCH 13/35] remove mocks since we're not using it anymore --- entrypoint.sh | 3 --- migrations/mocks/0005_mocks.sql | 46 --------------------------------- 2 files changed, 49 deletions(-) delete mode 100644 migrations/mocks/0005_mocks.sql diff --git a/entrypoint.sh b/entrypoint.sh index 72b1a1fb..c630ff82 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,12 +6,9 @@ export $(grep -v '^#' .env | xargs) # run db migrations echo "----- Running migrations..." cd migrations -cd mocks DB_CONFIG="host=$DB_HOST user=$DB_USERNAME dbname=$DB_NAME sslmode=disable password=$DB_PASSWORD" goose postgres "$DB_CONFIG" reset -cd .. -goose postgres "$DB_CONFIG" reset goose postgres "$DB_CONFIG" up cd .. echo "----- ...Migrations done" diff --git a/migrations/mocks/0005_mocks.sql b/migrations/mocks/0005_mocks.sql deleted file mode 100644 index f8f199ca..00000000 --- a/migrations/mocks/0005_mocks.sql +++ /dev/null @@ -1,46 +0,0 @@ -------------------------------------------------------------------------- --- +goose Up -------------------------------------------------------------------------- --- STRING_USER ---------------------------------------------------------- -INSERT INTO string_user (id, created_at, updated_at, type, status, tags, first_name, last_name) -VALUES ('0e837b73-55cf-43ff-9b1e-0d8258eec978', '2022-10-19 00:17:01.837572+00', '2022-10-19 00:17:01.837572+00', 'Developer', 'Developing', '{}', 'Deve', 'Loper'); - -------------------------------------------------------------------------- --- DEVICE --------------------------------------------------------------- -INSERT INTO device (id, created_at, updated_at, last_used_at, validated_at, description, user_id) -VALUES ('073f5a88-9223-4554-a7ce-11d358123a21', '2022-10-19 00:23:10.405595+00', '2022-10-19 00:23:10.405595+00', '2022-10-19 00:17:01.837572+00', '2022-10-19 00:17:01.837572+00', 'Developer Laptop', '0e837b73-55cf-43ff-9b1e-0d8258eec978'); - -------------------------------------------------------------------------- --- INSTRUMENT ----------------------------------------------------------- -INSERT INTO instrument (id, created_at, updated_at, type, status, tags, network, public_key, last_4, user_id) -VALUES ('13438963-f5e7-47c4-a790-ebca3e3bf915', '2022-10-19 00:53:36.538289+00', '2022-10-19 00:53:36.538289+00', 'Credit Card', 'Ephemeral', '{}', 'Mastercard', '', '4242', '0e837b73-55cf-43ff-9b1e-0d8258eec978'), -('ab6a2d66-ad4c-43f4-adf9-c0cd3282492c', '2022-10-19 00:55:26.166175+00', '2022-10-19 00:55:26.166175+00', 'Crypto Wallet', 'Ephemeral', '{}', 'Ethereum', '0x44A4b9E2A69d86BA382a511f845CbF2E31286770', '', '0e837b73-55cf-43ff-9b1e-0d8258eec978'); - -------------------------------------------------------------------------- --- NETWORK -------------------------------------------------------------- -INSERT INTO network (id, created_at, updated_at, name, network_id, chain_id, gas_token_id, gas_oracle, rpc_url, explorer_url) -VALUES ('ea34e526-ec6e-4f2b-89b4-acc08db80d63', '2022-10-14 20:18:09.555645+00', '2022-10-14 20:18:09.555645+00', 'Fuji Testnet', '43113', '43113', '19611d0e-a42f-4cee-a35a-b34eb5c08a7f', 'avax', 'https://api.avax-test.network/ext/bc/C/rpc', 'https://testnet.snowtrace.io'), -('b21d6cd6-5d8a-49a6-bac6-e6323316dc01', '2022-10-14 20:41:39.962327+00', '2022-10-14 20:41:39.962327+00', 'Goerli Testnet', '5', '5', '3ef72571-c2e1-4ca3-991c-0df17cef7535', 'eth', 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', 'https://goerli.etherscan.io'), -('6cea71b3-b287-4680-ad9d-e631d0bc84ba', '2022-10-14 20:41:39.962327+00', '2022-10-14 20:41:39.962327+00', 'Polygon Mainnet', '137', '137', 'c06986d8-cc2c-4cdc-9728-16a45698b3e7', 'poly', 'https://rpc-mainnet.matic.quiknode.pro', 'https://polygonscan.com'), -('cd42c066-554c-42ad-994b-48fed371931c', '2022-10-14 20:41:39.962327+00', '2022-10-14 20:41:39.962327+00', 'Avalanche Mainnet', '43114', '43114', '19611d0e-a42f-4cee-a35a-b34eb5c08a7f', 'avax', 'https://api.avax.network/ext/bc/C/rpc', 'https://snowtrace.io'), -('491d46e2-18e0-45ec-8209-faf0ec5d278c', '2022-10-14 20:41:39.962327+00', '2022-10-14 20:41:39.962327+00', 'Mumbai Testnet', '80001', '80001', 'c06986d8-cc2c-4cdc-9728-16a45698b3e7', 'poly', 'https://matic-mumbai.chainstacklabs.com', 'https://mumbai.polygonscan.com/'), -('60a02818-4e7d-4b84-b673-e2376fdbfbf9', '2022-10-14 20:41:39.962327+00', '2022-10-30 01:07:37.237054+00', 'Ethereum Mainnet', '1', '1', '3ef72571-c2e1-4ca3-991c-0df17cef7535', 'eth', 'https://rpc.ankr.com/eth', 'https://etherscan.io/'); - -------------------------------------------------------------------------- --- PLATFORM ------------------------------------------------------------- -INSERT INTO platform (id, created_at, updated_at, type, status, name, api_key, authentication) -VALUES ('54a7e062-4cec-44f3-9d89-99498d0eb6ef', '2022-10-19 00:37:16.965408+00', '2022-10-19 00:37:16.965408+00', 'Game', 'Verified', 'Nintendo', 'developer', 'email'); - -------------------------------------------------------------------------- --- ASSET ------------------------------------------------------------- -INSERT INTO asset (id, created_at, updated_at, name, description, decimals, is_crypto, network_id, value_oracle) -VALUES ('19611d0e-a42f-4cee-a35a-b34eb5c08a7f', '2022-10-14 20:17:06.460812+00', '2022-10-15 02:41:02.270712+00', 'AVAX', 'Avalanche', 18, TRUE, 'cd42c066-554c-42ad-994b-48fed371931c', 'avalanche-2'), -('c06986d8-cc2c-4cdc-9728-16a45698b3e7', '2022-10-14 20:17:06.460812+00', '2022-10-15 02:41:02.270712+00', 'MATIC', 'Matic', 18, TRUE, '6cea71b3-b287-4680-ad9d-e631d0bc84ba', 'matic-network'), -('3ef72571-c2e1-4ca3-991c-0df17cef7535', '2022-10-14 20:17:06.460812+00', '2022-10-15 02:41:02.270712+00', 'ETH', 'Ethereum', 18, TRUE, '60a02818-4e7d-4b84-b673-e2376fdbfbf9', 'ethereum'), -('bc376c3a-6481-49d0-83ef-34ba80937ba8', '2022-10-18 03:59:05.042924+00', '2022-10-18 03:59:05.042924+00', 'USD', 'United States Dollar', 6, FALSE, null, null); - - -------------------------------------------------------------------------- --- +goose Down - --- Can't delete rows due to foreign key constraint \ No newline at end of file From c0f9befda818adce9b235d98258e791fb1a84424 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 27 Jan 2023 11:43:21 -0700 Subject: [PATCH 14/35] make goose happy --- ...form_contact-to-platform_device-to-instrument_platform.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql index 0efdc4bc..0568dda5 100644 --- a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql +++ b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql @@ -57,8 +57,8 @@ ALTER TABLE platform DROP COLUMN IF EXISTS description, DROP COLUMN IF EXISTS domains, DROP COLUMN IF EXISTS ip_addresses, - ADD COLUMN type TEXT NOT NULL, -- enum: to be defined at struct level in Go - ADD COLUMN status TEXT NOT NULL, -- enum: to be defined at struct level in Go + ADD COLUMN type TEXT DEFAULT '', -- enum: to be defined at struct level in Go + ADD COLUMN status TEXT DEFAULT '', -- enum: to be defined at struct level in Go ADD COLUMN name TEXT DEFAULT '', ADD COLUMN api_key TEXT DEFAULT '', ADD COLUMN authentication TEXT DEFAULT ''; --enum [email, phone, wallet] From cb16928b0cd36dd1eeadbd2b2f3ab356850761cf Mon Sep 17 00:00:00 2001 From: Ocasta Date: Fri, 27 Jan 2023 12:46:24 -0600 Subject: [PATCH 15/35] rename to match many-to-many table names --- ... 0002_user-to-platform_device_contact_location_instrument.sql} | 0 ...ntact-to-platform_device-to-instrument_tx-leg_transaction.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename migrations/{0002_user-platform_device_contact_location_instrument.sql => 0002_user-to-platform_device_contact_location_instrument.sql} (100%) rename migrations/{0003_contact-platform_device-instrument_tx-leg_transaction.sql => 0003_contact-to-platform_device-to-instrument_tx-leg_transaction.sql} (100%) diff --git a/migrations/0002_user-platform_device_contact_location_instrument.sql b/migrations/0002_user-to-platform_device_contact_location_instrument.sql similarity index 100% rename from migrations/0002_user-platform_device_contact_location_instrument.sql rename to migrations/0002_user-to-platform_device_contact_location_instrument.sql diff --git a/migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql b/migrations/0003_contact-to-platform_device-to-instrument_tx-leg_transaction.sql similarity index 100% rename from migrations/0003_contact-platform_device-instrument_tx-leg_transaction.sql rename to migrations/0003_contact-to-platform_device-to-instrument_tx-leg_transaction.sql From f6fbc96e13f797da1e1e89727add994f4a2852f8 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:25:59 -0700 Subject: [PATCH 16/35] Update pkg/repository/platform.go Co-authored-by: akfoster --- pkg/repository/platform.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index f932da80..2bf6cc74 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -22,7 +22,6 @@ type Platform interface { GetById(ID string) (model.Platform, error) List(limit int, offset int) ([]model.Platform, error) Update(ID string, updates any) error - // GetByApiKey(key string) (model.Platform, error) } type platform[T any] struct { From 64571d16c7cabdc3862bf745bb01c8bd8ed16e69 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:26:14 -0700 Subject: [PATCH 17/35] Update pkg/repository/platform.go Co-authored-by: akfoster --- pkg/repository/platform.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/repository/platform.go b/pkg/repository/platform.go index 2bf6cc74..8d21f93d 100644 --- a/pkg/repository/platform.go +++ b/pkg/repository/platform.go @@ -52,13 +52,3 @@ func (p platform[T]) Create(m model.Platform) (model.Platform, error) { return plat, nil } -// func (p platform[T]) GetByApiKey(key string) (model.Platform, error) { -// m := model.Platform{} -// err := p.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE api_key = $1", p.table), key) -// if err != nil && err == sql.ErrNoRows { -// return m, common.StringError(ErrNotFound) -// } else if err != nil { -// return m, common.StringError(err) -// } -// return m, nil -// } From 5b9896f72479c20fdd6ac31c31efc022170e6962 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:26:23 -0700 Subject: [PATCH 18/35] Update pkg/service/platform.go Co-authored-by: akfoster --- pkg/service/platform.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/service/platform.go b/pkg/service/platform.go index 7e456f94..255ae2c7 100644 --- a/pkg/service/platform.go +++ b/pkg/service/platform.go @@ -23,12 +23,6 @@ func NewPlatform(repos repository.Repositories) Platform { func (a platform) Create(c CreatePlatform) (model.Platform, error) { uuiKey := "str." + uuidWithoutHyphens() hashed := common.ToSha256(uuiKey) - // m := model.Platform{ - // Type: c.Type, - // Authentication: c.Authentication, - // ApiKey: hashed, - // Status: "pending", - // } m := model.Platform{} plat, err := a.repos.Platform.Create(m) From a113ec39a23ccbb92596489016da459ba2b1ab0a Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Fri, 27 Jan 2023 12:26:31 -0700 Subject: [PATCH 19/35] Update pkg/service/platform.go Co-authored-by: akfoster --- pkg/service/platform.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/service/platform.go b/pkg/service/platform.go index 255ae2c7..aae5753f 100644 --- a/pkg/service/platform.go +++ b/pkg/service/platform.go @@ -32,7 +32,6 @@ func (a platform) Create(c CreatePlatform) (model.Platform, error) { _, err = a.repos.Auth.CreateAPIKey(plat.ID, c.Authentication, hashed, false) pt := &plat - // pt.ApiKey = uuiKey if err != nil { return *pt, common.StringError(err) } From 634558d5b3dde50ed17d6d5f54536291f1c1a0f3 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 27 Jan 2023 15:58:48 -0700 Subject: [PATCH 20/35] delete docker compose (we moved it to infra/local) and update readme accordingly --- README.md | 26 ++++++++++++----------- docker-compose.yml | 51 ---------------------------------------------- 2 files changed, 14 insertions(+), 63 deletions(-) delete mode 100644 docker-compose.yml diff --git a/README.md b/README.md index 3929e2ce..19f0392b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -### For Live Reloading: ### -1. install [Air|https://github.com/cosmtrek/air]: `go install github.com/cosmtrek/air@latest` -2. run `air` -3. if you get `zsh: command not found: air` you need to add to PATH: `PATH=$PATH:$(go env GOPATH)/bin` +### To run the APIS: ### +1. Ensure you have the infra repo where the docker compose file is now located +2. run `docker compose -f ../infra/local/docker-compose.yml up` +3. You can also run with the -d flag to keep the process in the background, ie `docker compose -f -d ../infra/local/docker-compose.yml up` -### For migrations: ### -1. install [Goose|https://pressly.github.io/] `brew install goose` -2. Note, this binary is separate from the go package. -3. `goose postgres "host=localhost dbname=string_db user=string_db password=string_password sslmode=disable" down-to 0` +### To get a live terminal output from any repository being run by the infra docker compose: ### +1. Run `docker logs [docker container name] -f` +2. ie `docker logs platform-admin-api -f` +3. You can get the container names using `docker ps` +4. If you don't want the output in real time, you can omit the `-f` flag -### Postgres & Redis - Docker Compose: ***local dev only*** ### -1. To build and start the docker containers for the first time: `docker-compose up --build` -2. To shutdown the docker containers press `ctl + c` +### For migrations: ### +1. This is now handled by the docker compose file ### Docker Issues? 1. If docker is giving you an error when you try to `docker-compose up --build` try the following commands in order: @@ -25,8 +25,10 @@ run `go install` to get dependencies installed ### For local testing: ### run `go test` +or if you want to run a specific test, use `go test -run [TestName] [./path/to/dir] -v -count 1` +ie `go test -run TestGetSwapPayload ./pkg/service -v -count 1` ### Unit21: ### This is a 3rd party service that offers the ability to evaluate risk at a transaction level and identify fraud. A client file exists to connect to their API. Documentation is here: https://docs.unit21.ai/reference/entities-api You can create a test API key on the Unit21 dashboard. You will need to be setup as an Admin. Here are the instructions: https://docs.unit21.ai/reference/generate-api-keys -When setting up the production env variables, their URL will be: https://api.unit21.com/v1 +When setting up the production env variables, their URL will be: https://api.unit21.com/v1 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 7108b7bc..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.8' -services: - string-api: - build: - context: . - dockerfile: dev.Dockerfile - container_name: string-api - ports: - - 5555:5555 - depends_on: - redis: - condition: service_started - db: - condition: service_healthy - volumes: - - ./:/string_api - db: - image: postgres:14.1-alpine - restart: always - environment: - - POSTGRES_USER=string_db - - POSTGRES_PASSWORD=string_password - ports: - - '5432:5432' - volumes: - - db:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U string_db"] - interval: 5s - timeout: 5s - retries: 5 - redis: - image: redis:7.0-alpine - restart: always - ports: - - '6379:6379' - command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81 - ## We want the Redis data not to persist. - ## Commenting out the lines below is equivalent to run the command FLUSHALL after each restart - ## Uncomment the following lines to persist data - # volumes: - # - redis:/data - -volumes: - db: - driver: local - ## We want the Redis data not to persist. - ## Commenting out the lines below is equivalent to run the command FLUSHALL after each restart - ## Uncomment the following lines to persist data - # redis: - # driver: local From 0d85205330b18140b56cf0aca2ac3e19ad9b73ec Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 27 Jan 2023 17:07:35 -0700 Subject: [PATCH 21/35] fix air install --- dev.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index 87be470f..77154435 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -12,8 +12,7 @@ ADD go.mod go.sum /string_api/ RUN go mod download # install the air tool -RUN curl -fLo install.sh https://raw.githubusercontent.com/cosmtrek/air/master/install.sh \ - && chmod +x install.sh && sh install.sh && cp ./bin/air /bin/air +RUN go install github.com/cosmtrek/air@latest # install goose for db migrations RUN go install github.com/pressly/goose/v3/cmd/goose@latest From c55bf9d2968c5a994ed01afc0d9195e5551c3f1c Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 30 Jan 2023 11:56:08 -0700 Subject: [PATCH 22/35] update migrations, env --- .env.example | 3 ++- ...tform_contact-to-platform_device-to-instrument_platform.sql | 2 +- ...latform_member-role_member-to-role_member-invite_apikey.sql | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index b6905fe4..1ba3384b 100644 --- a/.env.example +++ b/.env.example @@ -38,4 +38,5 @@ FINGERPRINT_API_URL=https://api.fpjs.io/ STRING_INTERNAL_ID=00000000-0000-0000-0000-000000000000 STRING_WALLET_ID=00000000-0000-0000-0000-000000000001 STRING_BANK_ID=00000000-0000-0000-0000-000000000002 -STRING_PLACEHOLDER_PLATFORM_ID=00000000-0000-0000-0000-000000000003 \ No newline at end of file +STRING_PLACEHOLDER_PLATFORM_ID=00000000-0000-0000-0000-000000000003 +SERVICE_NAME=string_api \ No newline at end of file diff --git a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql index 0568dda5..f2220fdd 100644 --- a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql +++ b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql @@ -41,7 +41,7 @@ ALTER TABLE platform DROP COLUMN IF EXISTS authentication, ADD COLUMN activated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, -- for activating prod users ADD COLUMN name TEXT NOT NULL, - ADD COLUMN description TEXT NOT NULL, + ADD COLUMN description TEXT DEFAULT '', ADD COLUMN domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) ADD COLUMN ip_addresses TEXT[] DEFAULT NULL; -- define which API ips can make calls (API-to-API) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index 43f665d9..d0f106a8 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -9,7 +9,7 @@ CREATE TABLE platform_member ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, - password TEXT NOT NULL -- how do we maintain this? + password TEXT DEFAULT '' -- how do we maintain this? ); ------------------------------------------------------------------------- From d59d2027977f2b0e281dba8987d965d7387597ad Mon Sep 17 00:00:00 2001 From: Marlon Monroy Date: Mon, 30 Jan 2023 18:45:03 -0800 Subject: [PATCH 23/35] fixed typo --- infra/dev/alb.tf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infra/dev/alb.tf b/infra/dev/alb.tf index 28cc53b0..be7f4443 100644 --- a/infra/dev/alb.tf +++ b/infra/dev/alb.tf @@ -23,6 +23,18 @@ resource "aws_alb" "alb" { create_before_destroy = true } } + + resource "aws_ssm_parameter" "alb" { + name = "${local.service_name}-alb-arn" + value = aws_alb.alb.arn + type = "String" + } + + resource "aws_ssm_parameter" "alb_dns" { + name = "${local.service_name}-alb-dns" + value = aws_alb.alb.dns_name + type = "String" + } resource "aws_alb_target_group" "ecs_task_target_group" { name = "${local.service_name}-tg" @@ -67,6 +79,12 @@ resource "aws_alb_listener" "alb_https_listener" { } } + resource "aws_ssm_parameter" "alb_listerner" { + name = "${local.service_name}-alb-listener-arn" + value = aws_alb_listener.alb_https_listener.arn + type = "String" + } + resource "aws_alb_listener_rule" "ecs_alb_listener_rule" { listener_arn = aws_alb_listener.alb_https_listener.arn priority = 100 From 1ce5015403a2ce584dd2f8825059855ca427c969 Mon Sep 17 00:00:00 2001 From: Wilfredo Alcala Date: Tue, 31 Jan 2023 19:42:51 -0400 Subject: [PATCH 24/35] STR-357 :: If fingerprint analytics fails, continue anyways (#99) --- api/api.go | 2 +- api/config.go | 7 +- api/handler/http_error.go | 7 ++ api/handler/login.go | 10 ++- api/handler/verification.go | 11 +-- pkg/model/entity.go | 1 + pkg/model/user.go | 6 +- pkg/service/auth.go | 81 +++++++++++----------- pkg/service/base.go | 1 + pkg/service/device.go | 130 ++++++++++++++++++++++++++++++++++++ pkg/service/user.go | 60 +++++++++-------- pkg/service/verification.go | 34 +++------- 12 files changed, 241 insertions(+), 109 deletions(-) create mode 100644 pkg/service/device.go diff --git a/api/api.go b/api/api.go index 468f346a..8ec6217a 100644 --- a/api/api.go +++ b/api/api.go @@ -101,7 +101,7 @@ func loginRoute(services service.Services, e *echo.Echo) { } func verificationRoute(services service.Services, e *echo.Echo) { - handler := handler.NewVerification(e, services.Verification) + handler := handler.NewVerification(e, services.Verification, services.Device) handler.RegisterRoutes(e.Group("/verification")) } diff --git a/api/config.go b/api/config.go index 22a974b5..9a0d8273 100644 --- a/api/config.go +++ b/api/config.go @@ -38,7 +38,11 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic verificationRepos := repository.Repositories{Contact: repos.Contact, User: repos.User, Device: repos.Device} verification := service.NewVerification(verificationRepos) - auth := service.NewAuth(repos, fingerprint, verification) + // device service + deviceRepos := repository.Repositories{Device: repos.Device} + device := service.NewDevice(deviceRepos, fingerprint) + + auth := service.NewAuth(repos, verification, device) apiKey := service.NewAPIKeyStrategy(repos.Auth) cost := service.NewCost(config.Redis) executor := service.NewExecutor() @@ -61,5 +65,6 @@ func NewServices(config APIConfig, repos repository.Repositories) service.Servic Transaction: transaction, User: user, Verification: verification, + Device: device, } } diff --git a/api/handler/http_error.go b/api/handler/http_error.go index 5a339812..044aa47d 100644 --- a/api/handler/http_error.go +++ b/api/handler/http_error.go @@ -90,3 +90,10 @@ func LinkExpired(c echo.Context, message ...string) error { } return c.JSON(http.StatusForbidden, JSONError{Message: "Forbidden", Code: "LINK_EXPIRED"}) } + +func InvalidEmail(c echo.Context, message ...string) error { + if len(message) > 0 { + return c.JSON(http.StatusUnprocessableEntity, JSONError{Message: strings.Join(message, " "), Code: "INVALID_EMAIL"}) + } + return c.JSON(http.StatusUnprocessableEntity, JSONError{Message: "Invalid email", Code: "INVALID_EMAIL"}) +} diff --git a/api/handler/login.go b/api/handler/login.go index dd1657e9..17098f97 100644 --- a/api/handler/login.go +++ b/api/handler/login.go @@ -67,10 +67,14 @@ func (l login) VerifySignature(c echo.Context) error { body.Nonce = string(decodedNonce) resp, err := l.Service.VerifySignedPayload(body) - if err != nil && strings.Contains(err.Error(), "unknown device") { - return Unprocessable(c) - } if err != nil { + if strings.Contains(err.Error(), "unknown device") { + return Unprocessable(c) + } + if strings.Contains(err.Error(), "invalid email") { + return InvalidEmail(c) + } + LogStringError(c, err, "login: verify signature") return BadRequestError(c, "Invalid Payload") } diff --git a/api/handler/verification.go b/api/handler/verification.go index 1dc092a5..02f2df34 100644 --- a/api/handler/verification.go +++ b/api/handler/verification.go @@ -19,12 +19,13 @@ type Verification interface { } type verification struct { - service service.Verification - group *echo.Group + service service.Verification + deviceService service.Device + group *echo.Group } -func NewVerification(route *echo.Echo, service service.Verification) Verification { - return &verification{service, nil} +func NewVerification(route *echo.Echo, service service.Verification, deviceService service.Device) Verification { + return &verification{service, deviceService, nil} } func (v verification) VerifyEmail(c echo.Context) error { @@ -39,7 +40,7 @@ func (v verification) VerifyEmail(c echo.Context) error { func (v verification) VerifyDevice(c echo.Context) error { token := c.QueryParam("token") - err := v.service.VerifyDevice(token) + err := v.deviceService.VerifyDevice(token) if err != nil { LogStringError(c, err, "verification: device verification") return BadRequestError(c) diff --git a/pkg/model/entity.go b/pkg/model/entity.go index d8a8e54f..edce1d9a 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -20,6 +20,7 @@ type User struct { FirstName string `json:"firstName" db:"first_name"` MiddleName string `json:"middleName" db:"middle_name"` LastName string `json:"lastName" db:"last_name"` + Email string `json:"email"` } // See PLATFORM in Migrations 0005 diff --git a/pkg/model/user.go b/pkg/model/user.go index c9e16855..ae622e16 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -10,12 +10,12 @@ type WalletSignaturePayload struct { } type FingerprintPayload struct { - VisitorID string `json:"visitorId" validate:"required"` - RequestID string `json:"requestId" validate:"required"` + VisitorID string `json:"visitorId"` + RequestID string `json:"requestId"` } type WalletSignaturePayloadSigned struct { Nonce string `json:"nonce" validate:"required"` Signature string `json:"signature" validate:"required"` - Fingerprint FingerprintPayload `json:"fingerprint" validate:"required"` + Fingerprint FingerprintPayload `json:"fingerprint"` } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index e1b825ad..e500d038 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -12,7 +12,6 @@ import ( "github.com/String-xyz/string-api/pkg/repository" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" - "github.com/lib/pq" "github.com/pkg/errors" ) @@ -51,7 +50,7 @@ type Auth interface { // if signaure is valid it returns a JWT to authenticate the user VerifySignedPayload(model.WalletSignaturePayloadSigned) (UserCreateResponse, error) - GenerateJWT(model.Device) (JWT, error) + GenerateJWT(string, ...model.Device) (JWT, error) ValidateAPIKey(key string) bool RefreshToken(token string, walletAddress string) (UserCreateResponse, error) InvalidateRefreshToken(token string) error @@ -59,13 +58,13 @@ type Auth interface { type auth struct { repos repository.Repositories - fingerprint Fingerprint verification Verification + device Device } // reusing UserRepos here -func NewAuth(r repository.Repositories, f Fingerprint, v Verification) Auth { - return &auth{r, f, v} +func NewAuth(r repository.Repositories, v Verification, d Device) Auth { + return &auth{r, v, d} } func (a auth) PayloadToSign(walletAddress string) (SignablePayload, error) { @@ -107,56 +106,36 @@ func (a auth) VerifySignedPayload(request model.WalletSignaturePayloadSigned) (U return resp, common.StringError(err) } - created, device, err := a.createDeviceIfNeeded(user.ID, request.Fingerprint.VisitorID, request.Fingerprint.RequestID) - if err != nil { + user.Email = getValidatedEmailOrEmpty(a.repos.Contact, user.ID) + + device, err := a.device.CreateDeviceIfNeeded(user.ID, request.Fingerprint.VisitorID, request.Fingerprint.RequestID) + if err != nil && !strings.Contains(err.Error(), "not found") { return resp, common.StringError(err) } - if created || device.ValidatedAt == nil { - go a.verification.SendDeviceVerification(user.ID, device.ID, device.Description) + // Send verification email if device is unknown and user has a validated email + if user.Email != "" && !isDeviceValidated(device) { + go a.verification.SendDeviceVerification(user.ID, user.Email, device.ID, device.Description) return resp, common.StringError(errors.New("unknown device")) } // Create the JWT - jwt, err := a.GenerateJWT(device) + jwt, err := a.GenerateJWT(user.ID, device) if err != nil { return resp, common.StringError(err) } - return UserCreateResponse{JWT: jwt, User: user}, nil -} -func (a auth) createDeviceIfNeeded(userID, visitorID, requestID string) (bool, model.Device, error) { - device, err := a.repos.Device.GetByUserIdAndFingerprint(userID, visitorID) - if err == nil { - return false, device, nil - } - // create device only if the error is not found - if err != nil && err == repository.ErrNotFound { - visitor, fpErr := a.fingerprint.GetVisitor(visitorID, requestID) - if fpErr != nil { - return false, model.Device{}, common.StringError(fpErr) - } - device, dErr := a.createDevice(userID, visitor) - return dErr == nil, device, dErr + // Invalidate device if it is unknown and was validated so it cannot be used again + err = a.device.InvalidateUnknownDevice(device) + if err != nil { + return resp, common.StringError(err) } - return false, device, common.StringError(err) -} - -func (a auth) createDevice(userID string, visitor model.FPVisitor) (model.Device, error) { - return a.repos.Device.Create(model.Device{ - UserID: userID, - Fingerprint: visitor.VisitorID, - Type: visitor.Type, - IpAddresses: pq.StringArray{visitor.IPAddress}, - Description: visitor.UserAgent, - LastUsedAt: time.Now(), - ValidatedAt: nil, - }) + return UserCreateResponse{JWT: jwt, User: user}, nil } // GenerateJWT generates a jwt token and a refresh token which is saved on redis -func (a auth) GenerateJWT(m model.Device) (JWT, error) { +func (a auth) GenerateJWT(userId string, m ...model.Device) (JWT, error) { claims := JWTClaims{} refreshToken := uuidWithoutHyphens() t := &JWT{ @@ -164,8 +143,12 @@ func (a auth) GenerateJWT(m model.Device) (JWT, error) { ExpAt: time.Now().Add(time.Minute * 15), } - claims.DeviceId = m.ID - claims.UserId = m.UserID + // set device id if available + if len(m) > 0 { + claims.DeviceId = m[0].ID + } + + claims.UserId = userId claims.ExpiresAt = t.ExpAt.Unix() claims.IssuedAt = t.IssuedAt.Unix() // replace this signing method with RSA or something similar @@ -177,7 +160,7 @@ func (a auth) GenerateJWT(m model.Device) (JWT, error) { t.Token = signed // create and save - refreshObj, err := a.repos.Auth.CreateJWTRefresh(common.ToSha256(refreshToken), m.UserID) + refreshObj, err := a.repos.Auth.CreateJWTRefresh(common.ToSha256(refreshToken), userId) if err != nil { return *t, err } @@ -240,7 +223,7 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreat } // create new jwt - jwt, err := a.GenerateJWT(device) + jwt, err := a.GenerateJWT(userId, device) if err != nil { return resp, common.StringError(err) } @@ -256,6 +239,9 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreat if err != nil { return resp, common.StringError(err) } + + // get email + user.Email = getValidatedEmailOrEmpty(a.repos.Contact, user.ID) resp.User = user return resp, nil @@ -295,3 +281,12 @@ func uuidWithoutHyphens() string { s := uuid.New().String() return strings.Replace(s, "-", "", -1) } + +func getValidatedEmailOrEmpty(contactRepo repository.Contact, userId string) string { + contact, err := contactRepo.GetByUserIdAndStatus(userId, "validated") + if err != nil { + return "" + } + + return contact.Data +} diff --git a/pkg/service/base.go b/pkg/service/base.go index c532551c..ee055faf 100644 --- a/pkg/service/base.go +++ b/pkg/service/base.go @@ -13,4 +13,5 @@ type Services struct { Transaction Transaction User User Verification Verification + Device Device } diff --git a/pkg/service/device.go b/pkg/service/device.go new file mode 100644 index 00000000..de5ca877 --- /dev/null +++ b/pkg/service/device.go @@ -0,0 +1,130 @@ +package service + +import ( + "os" + "time" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/lib/pq" + "github.com/pkg/errors" +) + +type Device interface { + VerifyDevice(encrypted string) error + CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error) + CreateUnknownDevice(userID string) (model.Device, error) + InvalidateUnknownDevice(device model.Device) error +} + +type device struct { + repos repository.Repositories + fingerprint Fingerprint +} + +func NewDevice(repos repository.Repositories, f Fingerprint) Device { + return &device{repos, f} +} + +func (d device) createDevice(userID string, visitor model.FPVisitor, description string) (model.Device, error) { + return d.repos.Device.Create(model.Device{ + UserID: userID, + Fingerprint: visitor.VisitorID, + Type: visitor.Type, + IpAddresses: pq.StringArray{visitor.IPAddress}, + Description: description, + LastUsedAt: time.Now(), + }) +} + +func (d device) CreateUnknownDevice(userID string) (model.Device, error) { + visitor := model.FPVisitor{ + VisitorID: "unknown", + Type: "unknown", + IPAddress: "unknown", + UserAgent: "unknown", + } + device, err := d.createDevice(userID, visitor, "an unknown device") + return device, common.StringError(err) +} + +func (d device) CreateDeviceIfNeeded(userID, visitorID, requestID string) (model.Device, error) { + if visitorID == "" || requestID == "" { + /* fingerprint is not available, create an unknown device. It should be invalidated on every login */ + device, err := d.getOrCreateUnknownDevice(userID, "unknown") + if err != nil { + return device, common.StringError(err) + } + + if !isDeviceValidated(device) { + device.ValidatedAt = nil + return device, nil + } + + return device, common.StringError(err) + } else { + /* device recognized, create or get the device */ + device, err := d.repos.Device.GetByUserIdAndFingerprint(userID, visitorID) + if err == nil { + return device, err + } + + /* create device only if the error is not found */ + if err == repository.ErrNotFound { + visitor, fpErr := d.fingerprint.GetVisitor(visitorID, requestID) + if fpErr != nil { + return model.Device{}, common.StringError(fpErr) + } + device, dErr := d.createDevice(userID, visitor, "a new device "+visitor.UserAgent+" ") + return device, dErr + } + + return device, common.StringError(err) + } +} + +func (d device) VerifyDevice(encrypted string) error { + key := os.Getenv("STRING_ENCRYPTION_KEY") + received, err := common.Decrypt[DeviceVerification](encrypted, key) + if err != nil { + return common.StringError(err) + } + + now := time.Now() + if now.Unix()-received.Timestamp > (60 * 15) { + return common.StringError(errors.New("link expired")) + } + err = d.repos.Device.Update(received.DeviceID, model.DeviceUpdates{ValidatedAt: &now}) + return err +} + +func (d device) getOrCreateUnknownDevice(userId, visitorId string) (model.Device, error) { + var device model.Device + + device, err := d.repos.Device.GetByUserIdAndFingerprint(userId, "unknown") + if err != nil && err != repository.ErrNotFound { + return device, common.StringError(err) + } + + if device.ID != "" { + return device, nil + } + + // if device is not found, create a new one + device, err = d.CreateUnknownDevice(userId) + return device, common.StringError(err) +} + +func isDeviceValidated(device model.Device) bool { + return device.ValidatedAt != nil && !device.ValidatedAt.IsZero() +} + +func (d device) InvalidateUnknownDevice(device model.Device) error { + if device.Fingerprint != "unknown" { + return nil // only unknown devices can be invalidated + } + + device.ValidatedAt = &time.Time{} // Zero time to set it to nil + return d.repos.Device.Update(device.ID, device) +} diff --git a/pkg/service/user.go b/pkg/service/user.go index f30e53e1..1d205ac1 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -93,12 +93,38 @@ func (u user) Create(request model.WalletSignaturePayloadSigned) (UserCreateResp return resp, common.StringError(err) } - user, device, err := u.createUserData(addr, request.Fingerprint.VisitorID, request.Fingerprint.RequestID) + user, err := u.createUserData(addr) if err != nil { return resp, err } - jwt, err := u.auth.GenerateJWT(device) + var device model.Device + + // create device only if there is a visitor + visitorID := request.Fingerprint.VisitorID + requestID := request.Fingerprint.RequestID + if visitorID != "" && requestID != "" { + visitor, err := u.fingerprint.GetVisitor(visitorID, requestID) + if err == nil { + // if fingerprint successfully retrieved, create device, otherwise continue without device + now := time.Now() + + device, err = u.repos.Device.Create(model.Device{ + Fingerprint: visitorID, + UserID: user.ID, + Type: visitor.Type, + IpAddresses: pq.StringArray{visitor.IPAddress}, + Description: visitor.UserAgent, + LastUsedAt: now, + ValidatedAt: &now, + }) + if err != nil { + return resp, common.StringError(err) + } + } + } + + jwt, err := u.auth.GenerateJWT(user.ID, device) if err != nil { return resp, common.StringError(err) } @@ -109,7 +135,7 @@ func (u user) Create(request model.WalletSignaturePayloadSigned) (UserCreateResp return UserCreateResponse{JWT: jwt, User: user}, nil } -func (u user) createUserData(addr, visitorID, requestID string) (model.User, model.Device, error) { +func (u user) createUserData(addr string) (model.User, error) { tx := u.repos.User.MustBegin() u.repos.Instrument.SetTx(tx) u.repos.Device.SetTx(tx) @@ -121,41 +147,21 @@ func (u user) createUserData(addr, visitorID, requestID string) (model.User, mod user, err := u.repos.User.Create(user) if err != nil { u.repos.User.Rollback() - return user, model.Device{}, common.StringError(err) + return user, common.StringError(err) } // Create a new wallet instrument and associate it with the new user instrument := model.Instrument{Type: "crypto-wallet", Status: "verified", Network: "EVM", PublicKey: addr, UserID: user.ID} instrument, err = u.repos.Instrument.Create(instrument) if err != nil { u.repos.Instrument.Rollback() - return user, model.Device{}, common.StringError(err) - } - - visitor, err := u.fingerprint.GetVisitor(visitorID, requestID) - if err != nil { - u.repos.Instrument.Rollback() - return user, model.Device{}, err // is this intentionally not common.StringError? - } - now := time.Now() - device, err := u.repos.Device.Create(model.Device{ - Fingerprint: visitorID, - UserID: user.ID, - Type: visitor.Type, - IpAddresses: pq.StringArray{visitor.IPAddress}, - Description: visitor.UserAgent, - LastUsedAt: now, - ValidatedAt: &now, - }) - if err != nil { - u.repos.Device.Rollback() - return user, model.Device{}, err // is this intentionally not common.StringError? + return user, common.StringError(err) } if err := u.repos.User.Commit(); err != nil { - return user, model.Device{}, common.StringError(errors.New("error commiting transaction")) + return user, common.StringError(errors.New("error commiting transaction")) } - return user, device, nil + return user, nil } func (u user) Update(userID string, request UserUpdates) (model.User, error) { diff --git a/pkg/service/verification.go b/pkg/service/verification.go index c9751ed1..e91059dd 100644 --- a/pkg/service/verification.go +++ b/pkg/service/verification.go @@ -34,8 +34,7 @@ type Verification interface { // VerifyEmail verifies the provided email and creates a contact VerifyEmail(encrypted string) error - SendDeviceVerification(userID string, deviceID string, deviceDescription string) error - VerifyDevice(encrypted string) error + SendDeviceVerification(userID, email string, deviceID string, deviceDescription string) error } type verification struct { @@ -109,13 +108,8 @@ func (v verification) SendEmailVerification(userID, email string) error { return common.StringError(errors.New("link expired")) } -func (v verification) SendDeviceVerification(userID, deviceID, deviceDescription string) error { - email, err := v.repos.Contact.GetByUserIdAndStatus(userID, "validated") - if err != nil { - log.Err(err).Msg("Error getting a valid email") - return err - } - log.Info().Str("email", email.Data) +func (v verification) SendDeviceVerification(userID, email, deviceID, deviceDescription string) error { + log.Info().Str("email", email) key := os.Getenv("STRING_ENCRYPTION_KEY") code, err := common.Encrypt(DeviceVerification{Timestamp: time.Now().Unix(), DeviceID: deviceID, UserID: userID}, key) if err != nil { @@ -126,11 +120,13 @@ func (v verification) SendDeviceVerification(userID, deviceID, deviceDescription baseURL := common.GetBaseURL() from := mail.NewEmail("String XYZ", "auth@string.xyz") subject := "New Device Login Verification" - to := mail.NewEmail("New Device Login", email.Data) + to := mail.NewEmail("New Device Login", email) link := baseURL + "verification?type=device&token=" + code - htmlContent := fmt.Sprintf(`
We noticed that you attempted to log in from a new device %s. Is this you?
+ + textContent := "We noticed that you attempted to log in from " + deviceDescription + " at " + time.Now().Local().Format(time.RFC1123) + ". Is this you?" + htmlContent := fmt.Sprintf(`
%s
Yes`, - deviceDescription, link) + textContent, link) message := mail.NewSingleEmail(from, subject, to, "", htmlContent) client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY")) @@ -168,17 +164,3 @@ func (v verification) VerifyEmail(encrypted string) error { return nil } - -func (v verification) VerifyDevice(encrypted string) error { - key := os.Getenv("STRING_ENCRYPTION_KEY") - received, err := common.Decrypt[DeviceVerification](encrypted, key) - if err != nil { - return common.StringError(err) - } - now := time.Now() - if now.Unix()-received.Timestamp > (60 * 15) { - return common.StringError(errors.New("link expired")) - } - err = v.repos.Device.Update(received.DeviceID, model.DeviceUpdates{ValidatedAt: &now}) - return err -} From 164132b11fc4fbad4a8560045a0c1a6a5d8ce692 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 2 Feb 2023 11:45:26 -0700 Subject: [PATCH 25/35] add 'name' to platform_member and member_invite tables --- ...form_member-role_member-to-role_member-invite_apikey.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index d0f106a8..76bb5eff 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -9,7 +9,8 @@ CREATE TABLE platform_member ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, - password TEXT DEFAULT '' -- how do we maintain this? + password TEXT DEFAULT, '' -- how do we maintain this? + name TEXT DEFAULT '' ); ------------------------------------------------------------------------- @@ -51,7 +52,8 @@ CREATE TABLE member_invite ( accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, invited_by UUID REFERENCES platform_member (id), - platform_id UUID REFERENCES platform (id) + platform_id UUID REFERENCES platform (id), + name TEXT DEFAULT '' ); ------------------------------------------------------------------------- From d8e21117a9bc619393ca60e51e282d3371c88bce Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 2 Feb 2023 11:53:45 -0700 Subject: [PATCH 26/35] moved comma --- ...platform_member-role_member-to-role_member-invite_apikey.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index 76bb5eff..0af57b6c 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -9,7 +9,7 @@ CREATE TABLE platform_member ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, - password TEXT DEFAULT, '' -- how do we maintain this? + password TEXT DEFAULT '', -- how do we maintain this? name TEXT DEFAULT '' ); From cdd132ae607cb25b03bc43b2118a9e431145516a Mon Sep 17 00:00:00 2001 From: akfoster Date: Mon, 6 Feb 2023 12:22:49 -0600 Subject: [PATCH 27/35] Get Unit21 Real Time Rules engine and Checkout approvals working (#104) * work in progress * U21 RTR working! * if transaction fails due to risk, return 422 * get unit21 tests working again * starting point for new Evaluate tests * missed new evaluate test file * got rule working properly, attending to many linked cards rule * refactor for code reuse * move rtr url to env var * properly handling checkout authorization declines * creating instrument action in unit21 * most tests working besides the one blocked by slow unit21 data ingestion --- .env.example | 1 + api/handler/transact.go | 7 + pkg/internal/common/util.go | 21 +++ pkg/internal/unit21/action.go | 99 +++++++++++ pkg/internal/unit21/base.go | 7 +- pkg/internal/unit21/entity.go | 5 +- pkg/internal/unit21/entity_test.go | 175 ++++++++++++------- pkg/internal/unit21/evaluate_test.go | 188 +++++++++++++++++++++ pkg/internal/unit21/instrument.go | 3 - pkg/internal/unit21/instrument_test.go | 82 ++------- pkg/internal/unit21/transaction.go | 68 +++++--- pkg/internal/unit21/transaction_test.go | 211 +++++++---------------- pkg/internal/unit21/types.go | 18 +- pkg/model/entity.go | 38 ++--- pkg/model/request.go | 9 + pkg/repository/location_test.go | 2 +- pkg/repository/user_test.go | 2 +- pkg/service/chain.go | 3 +- pkg/service/checkout.go | 31 +++- pkg/service/transaction.go | 215 +++++++++++++++--------- 20 files changed, 763 insertions(+), 422 deletions(-) create mode 100644 pkg/internal/unit21/action.go create mode 100644 pkg/internal/unit21/evaluate_test.go diff --git a/.env.example b/.env.example index 1ba3384b..6e9a8bd3 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ JWT_SECRET_KEY=hskdhfjfkdhsgafgwterurorhfh UNIT21_API_KEY= UNIT21_ENV=sandbox2-api UNIT21_ORG_NAME=string +UNIT21_RTR_URL=https://rtr.sandbox2.unit21.com/evaluate TWILIO_ACCOUNT_SID=AC034879a536d54325687e48544403cb4d TWILIO_AUTH_TOKEN= TWILIO_SMS_SID=MG367a4f51ea6f67a28db4d126eefc734f diff --git a/api/handler/transact.go b/api/handler/transact.go index 0178fb3d..2a4c56c5 100644 --- a/api/handler/transact.go +++ b/api/handler/transact.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "strings" "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/service" @@ -29,6 +30,7 @@ func (t transaction) Transact(c echo.Context) error { LogStringError(c, err, "transact: execute bind") return BadRequestError(c) } + SanitizeChecksums(&body.CxAddr, &body.UserAddress) // Sanitize Checksum for body.CxParams? It might look like this: for i := range body.CxParams { @@ -37,10 +39,15 @@ func (t transaction) Transact(c echo.Context) error { userId := c.Get("userId").(string) deviceId := c.Get("deviceId").(string) res, err := t.Service.Execute(body, userId, deviceId) + if err != nil && (strings.Contains(err.Error(), "risk:") || strings.Contains(err.Error(), "payment:")) { + LogStringError(c, err, "transact: execute") + return Unprocessable(c) + } if err != nil { LogStringError(c, err, "transact: execute") return InternalError(c) } + return c.JSON(http.StatusOK, res) } diff --git a/pkg/internal/common/util.go b/pkg/internal/common/util.go index 91b71606..15041d88 100644 --- a/pkg/internal/common/util.go +++ b/pkg/internal/common/util.go @@ -1,9 +1,12 @@ package common import ( + "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" + "io/ioutil" "log" "math" "os" @@ -94,3 +97,21 @@ func FloatToUSDString(amount float64) string { func IsLocalEnv() bool { return os.Getenv("ENV") == "local" } + +func BetterStringify(jsonBody any) (betterString string, err error) { + bodyBytes, err := json.Marshal(jsonBody) + if err != nil { + log.Printf("Could not encode %+v to bytes: %s", jsonBody, err) + return betterString, StringError(err) + } + + bodyReader := bytes.NewReader(bodyBytes) + + betterBytes, err := ioutil.ReadAll(bodyReader) + betterString = string(betterBytes) + if err != nil { + return betterString, StringError(err) + } + + return +} diff --git a/pkg/internal/unit21/action.go b/pkg/internal/unit21/action.go new file mode 100644 index 00000000..e313fe5f --- /dev/null +++ b/pkg/internal/unit21/action.go @@ -0,0 +1,99 @@ +package unit21 + +import ( + "encoding/json" + "log" + "os" + + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type Action interface { + Create(instrument model.Instrument, + actionDetails string, + unit21InstrumentId string, + eventSubtype string) (unit21Id string, err error) +} + +type ActionRepo struct { + User repository.User + Device repository.Device + Location repository.Location +} + +type action struct { + repo ActionRepo +} + +func NewAction(r ActionRepo) Action { + return &action{repo: r} +} + +func (a action) Create( + instrument model.Instrument, + actionDetails string, + unit21InstrumentId string, + eventSubtype string) (unit21Id string, err error) { + + actionData := actionData{ + ActionType: instrument.Type, + ActionDetails: actionDetails, + EntityId: instrument.UserID, + EntityType: "user", + InstrumentId: instrument.ID, + } + + url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/events/create" + body, err := u21Post(url, mapToUnit21ActionEvent(instrument, actionData, unit21InstrumentId, eventSubtype)) + if err != nil { + log.Printf("Unit21 Action create failed: %s", err) + return "", common.StringError(err) + } + + var u21Response *createEventResponse + err = json.Unmarshal(body, &u21Response) + if err != nil { + log.Printf("Reading body failed: %s", err) + return "", common.StringError(err) + } + + log.Printf("Create Action Unit21Id: %s", u21Response.Unit21Id) + + return u21Response.Unit21Id, nil +} + +func mapToUnit21ActionEvent(instrument model.Instrument, actionData actionData, unit21Id string, eventSubtype string) *u21Event { + var instrumentTagArr []string + if instrument.Tags != nil { + for key, value := range instrument.Tags { + instrumentTagArr = append(instrumentTagArr, key+":"+value) + } + } + + jsonBody := &u21Event{ + GeneralData: &eventGeneral{ + EventId: unit21Id, //required + EventType: "action", //required + EventTime: int(instrument.CreatedAt.Unix()), //required + EventSubtype: eventSubtype, //required for RTR + Status: instrument.Status, + Parents: nil, + Tags: instrumentTagArr, + }, + ActionData: &actionData, + DigitalData: nil, + LocationData: nil, + CustomData: nil, + } + + actionBody, err := common.BetterStringify(jsonBody) + if err != nil { + log.Printf("\nError creating action body\n") + return jsonBody + } + log.Printf("\nCreate Action action body: %+v\n", actionBody) + + return jsonBody +} diff --git a/pkg/internal/unit21/base.go b/pkg/internal/unit21/base.go index 35c5663c..1f0e6c89 100644 --- a/pkg/internal/unit21/base.go +++ b/pkg/internal/unit21/base.go @@ -51,8 +51,6 @@ func u21Put(url string, jsonBody any) (body []byte, err error) { return nil, common.StringError(err) } - log.Printf("String of body from response: %s", string(body)) - if res.StatusCode != 200 { log.Printf("Request failed to update %s: %s", url, fmt.Sprint(res.StatusCode)) err = common.StringError(fmt.Errorf("request failed with status code %s and return body: %s", fmt.Sprint(res.StatusCode), string(body))) @@ -64,8 +62,9 @@ func u21Put(url string, jsonBody any) (body []byte, err error) { func u21Post(url string, jsonBody any) (body []byte, err error) { apiKey := os.Getenv("UNIT21_API_KEY") - log.Printf("jsonBody: %+v", jsonBody) + reqBodyBytes, err := json.Marshal(jsonBody) + if err != nil { log.Printf("Could not encode %+v to bytes: %s", jsonBody, err) return nil, common.StringError(err) @@ -95,7 +94,7 @@ func u21Post(url string, jsonBody any) (body []byte, err error) { body, err = ioutil.ReadAll(res.Body) if err != nil { - log.Printf("Error extracting body from %s update request: %s", url, err) + log.Printf("Error extracting body from %s update response: %s", url, err) return nil, common.StringError(err) } diff --git a/pkg/internal/unit21/entity.go b/pkg/internal/unit21/entity.go index 3437e593..e0c3820a 100644 --- a/pkg/internal/unit21/entity.go +++ b/pkg/internal/unit21/entity.go @@ -128,15 +128,13 @@ func (e entity) AddInstruments(entityId string, instrumentIds []string) (err err instruments := make(map[string][]string) instruments["instrument_ids"] = instrumentIds - body, err := u21Put(url, instruments) + _, err = u21Put(url, instruments) if err != nil { log.Printf("Unit21 Entity Add Instruments failed: %s", err) err = common.StringError(err) return } - log.Printf("String of body from response: %s", string(body)) - return } @@ -186,7 +184,6 @@ func (e entity) getCustomData(userId string) (customData entityCustomData, err e for _, platform := range devices { customData.Platforms = append(customData.Platforms, platform.PlatformID) } - log.Printf("deviceData: %s", customData) return } diff --git a/pkg/internal/unit21/entity_test.go b/pkg/internal/unit21/entity_test.go index 8aef5517..7e387e9b 100644 --- a/pkg/internal/unit21/entity_test.go +++ b/pkg/internal/unit21/entity_test.go @@ -1,6 +1,7 @@ package unit21 import ( + "database/sql" "testing" "time" @@ -15,17 +16,30 @@ import ( ) func TestCreateEntity(t *testing.T) { - err := godotenv.Load("../../../.env") + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) + defer db.Close() - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } + _, u21EntityId, err := createMockUser(mock, sqlxDB) + + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21EntityId)), 0) + + //validate response from Unit21 + //check Unit21 dashboard for new entity added + // TODO: mock call to client once it's manually tested +} + +func TestUpdateEntity(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) defer db.Close() - entityId := uuid.NewString() + // create entity to modify + entityId, u21EntityId, err := createMockUser(mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21EntityId)), 0) + user := model.User{ ID: entityId, CreatedAt: time.Now(), @@ -33,7 +47,7 @@ func TestCreateEntity(t *testing.T) { DeactivatedAt: nil, Type: "User", Status: "Onboarded", - Tags: model.StringMap{"platform": "Activision Blizzard"}, + Tags: nil, FirstName: "Test", MiddleName: "A", LastName: "User", @@ -49,17 +63,18 @@ func TestCreateEntity(t *testing.T) { mockedUserPlatformRow := sqlmock.NewRows([]string{"user_id", "platform_id"}). AddRow(entityId, uuid.NewString()) - mock.ExpectQuery("SELECT * FROM user_platform WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(entityId, 100, 0).WillReturnRows(mockedUserPlatformRow) + mock.ExpectQuery("SELECT * FROM user_to_platform WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(entityId, 100, 0).WillReturnRows(mockedUserPlatformRow) repos := EntityRepos{ - Device: repository.NewDevice(sqlxDB), - Contact: repository.NewContact(sqlxDB), - UserPlatform: repository.NewUserPlatform(sqlxDB), + Device: repository.NewDevice(sqlxDB), + Contact: repository.NewContact(sqlxDB), + UserToPlatform: repository.NewUserToPlatform(sqlxDB), } u21Entity := NewEntity(repos) - u21EntityId, err := u21Entity.Create(user) + // update in u21 + u21EntityId, err = u21Entity.Update(user) assert.NoError(t, err) assert.Greater(t, len([]rune(u21EntityId)), 0) @@ -68,19 +83,41 @@ func TestCreateEntity(t *testing.T) { // TODO: mock call to client once it's manually tested } -func TestUpdateEntity(t *testing.T) { - err := godotenv.Load("../../../.env") +func TestAddInstruments(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) + defer db.Close() - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) + // create entity to modify + entityId, u21EntityId, err := createMockUser(mock, sqlxDB) + + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21EntityId)), 0) + + var instrumentIds []string + + // mock new instrumentIds + for i := 1; i <= 10; i++ { + instrumentIds = append(instrumentIds, uuid.NewString()) } - defer db.Close() - // choose an older entity so we can update it - entityId := "d8451ddd-6116-4b62-9072-0e8b63a843f1" + repos := EntityRepos{ + Device: repository.NewDevice(sqlxDB), + Contact: repository.NewContact(sqlxDB), + UserToPlatform: repository.NewUserToPlatform(sqlxDB), + } + + u21Entity := NewEntity(repos) + err = u21Entity.AddInstruments(entityId, instrumentIds) + assert.NoError(t, err) + + //validate response from Unit21 + //check Unit21 dashboard for new entity added + // TODO: mock call to client once it's manually tested +} + +func createMockUser(mock sqlmock.Sqlmock, sqlxDB *sqlx.DB) (entityId string, unit21Id string, err error) { + entityId = uuid.NewString() user := model.User{ ID: entityId, CreatedAt: time.Now(), @@ -88,7 +125,7 @@ func TestUpdateEntity(t *testing.T) { DeactivatedAt: nil, Type: "User", Status: "Onboarded", - Tags: nil, + Tags: model.StringMap{"platform": "Activision Blizzard"}, FirstName: "Test", MiddleName: "A", LastName: "User", @@ -104,57 +141,79 @@ func TestUpdateEntity(t *testing.T) { mockedUserPlatformRow := sqlmock.NewRows([]string{"user_id", "platform_id"}). AddRow(entityId, uuid.NewString()) - mock.ExpectQuery("SELECT * FROM user_platform WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(entityId, 100, 0).WillReturnRows(mockedUserPlatformRow) + mock.ExpectQuery("SELECT * FROM user_to_platform WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(entityId, 100, 0).WillReturnRows(mockedUserPlatformRow) repos := EntityRepos{ - Device: repository.NewDevice(sqlxDB), - Contact: repository.NewContact(sqlxDB), - UserPlatform: repository.NewUserPlatform(sqlxDB), + Device: repository.NewDevice(sqlxDB), + Contact: repository.NewContact(sqlxDB), + UserToPlatform: repository.NewUserToPlatform(sqlxDB), } u21Entity := NewEntity(repos) - // update in u21 - u21EntityId, err := u21Entity.Update(user) - assert.NoError(t, err) - assert.Greater(t, len([]rune(u21EntityId)), 0) + u21EntityId, err := u21Entity.Create(user) - //validate response from Unit21 - //check Unit21 dashboard for new entity added - // TODO: mock call to client once it's manually tested + return entityId, u21EntityId, err } -func TestAddInstruments(t *testing.T) { - err := godotenv.Load("../../../.env") - assert.NoError(t, err) +func createMockInstrumentForUser(userId string, mock sqlmock.Sqlmock, sqlxDB *sqlx.DB) (instrument model.Instrument, unit21Id string, err error) { + instrumentId := uuid.NewString() + locationId := uuid.NewString() - db, _, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) + instrument = model.Instrument{ + ID: instrumentId, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + DeactivatedAt: nil, + Type: "Credit Card", + Status: "Verified", + Tags: nil, + Network: "Visa", + PublicKey: "", + Last4: "1234", + UserID: userId, + LocationID: sql.NullString{String: locationId}, } - defer db.Close() - entityId := "44142758-f015-4f79-a004-e554b0641480" //previous created test user - var instrumentIds []string + mockedUserRow1 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). + AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") + mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND deactivated_at IS NULL").WithArgs(userId).WillReturnRows(mockedUserRow1) - // mock new instrumentIds - for i := 1; i <= 10; i++ { - instrumentIds = append(instrumentIds, uuid.NewString()) - } + mockedUserRow2 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). + AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") + mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND deactivated_at IS NULL").WithArgs(userId).WillReturnRows(mockedUserRow2) - repos := EntityRepos{ - Device: repository.NewDevice(sqlxDB), - Contact: repository.NewContact(sqlxDB), - UserPlatform: repository.NewUserPlatform(sqlxDB), + mockedDeviceRow := sqlmock.NewRows([]string{"id", "type", "description", "fingerprint", "ip_addresses", "user_id"}). + AddRow(uuid.NewString(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"187.25.24.128"}, userId) + mock.ExpectQuery("SELECT * FROM device WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(userId, 100, 0).WillReturnRows(mockedDeviceRow) + + mockedLocationRow := sqlmock.NewRows([]string{"id", "type", "status", "building_number", "unit_number", "street_name", "city", "state", "postal_code", "country"}). + AddRow(locationId, "Home", "Verified", "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") + mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND deactivated_at IS NULL").WithArgs(locationId).WillReturnRows(mockedLocationRow) + + repo := InstrumentRepo{ + User: repository.NewUser(sqlxDB), + Device: repository.NewDevice(sqlxDB), + Location: repository.NewLocation(sqlxDB), } - u21Entity := NewEntity(repos) + u21Instrument := NewInstrument(repo) - err = u21Entity.AddInstruments(entityId, instrumentIds) - assert.NoError(t, err) + u21InstrumentId, err := u21Instrument.Create(instrument) - //validate response from Unit21 - //check Unit21 dashboard for new entity added - // TODO: mock call to client once it's manually tested + return instrument, u21InstrumentId, err +} + +func initializeTest(t *testing.T) (db *sql.DB, mock sqlmock.Sqlmock, sqlxDB *sqlx.DB, err error) { + err = godotenv.Load("../../../.env") + if err != nil { + t.Fatalf("error %s was not expected when loading env", err) + } + + db, mock, err = sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + sqlxDB = sqlx.NewDb(db, "sqlmock") + if err != nil { + t.Fatalf("error %s was not expected when opening stub db", err) + } + return } diff --git a/pkg/internal/unit21/evaluate_test.go b/pkg/internal/unit21/evaluate_test.go new file mode 100644 index 00000000..fd108ff5 --- /dev/null +++ b/pkg/internal/unit21/evaluate_test.go @@ -0,0 +1,188 @@ +package unit21 + +import ( + "fmt" + "testing" + "time" + + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" +) + +// This transaction should pass +func TestEvaluateTransactionPass(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) + defer db.Close() + + userId := uuid.NewString() + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.True(t, pass) +} + +// Entity makes a credit card purchase over $1,500 +func TestEvaluateTransactionAbnormalAmounts(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) + defer db.Close() + + userId := uuid.NewString() + transaction := createMockTransactionForUser(userId, "2000000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.False(t, pass) +} + +// User links more than 5 cards to their account in a 1 hour span +// Not currently functioning due to lag in Unit21 data ingestion +func TestEvaluateTransactionManyLinkedCards(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) + defer db.Close() + + userId, u21UserId, err := createMockUser(mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21UserId)), 0) + + // create 6 instruments + for i := 0; i <= 5; i++ { + var u21InstrumentId string + instrument, u21InstrumentId, err := createMockInstrumentForUser(userId, mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21InstrumentId)), 0) + + // Log create instrument action w/ Unit21 + u21ActionRepo := ActionRepo{ + User: repository.NewUser(sqlxDB), + Device: repository.NewDevice(sqlxDB), + Location: repository.NewLocation(sqlxDB), + } + + u21Action := NewAction(u21ActionRepo) + _, err = u21Action.Create(instrument, "Creation", u21InstrumentId, "Creation") + if err != nil { + fmt.Printf("Error creating a new instrument action in Unit21") + return + } + } + + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + time.Sleep(10 * time.Second) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.False(t, pass) +} + +// 10 or more FAILED transactions in a 1 hour span +func TestEvaluateTransactionHighFailedTransactionAmount(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) + defer db.Close() + + userId, u21UserId, err := createMockUser(mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21UserId)), 0) + // create, evaluate, and execute 10 failed transactions + for i := 0; i < 10; i++ { + transaction := createMockTransactionForUser(userId, "2000000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.False(t, pass) + transaction.Status = "Failed" + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + u21TransactionId, err := executeMockTransactionForUser(transaction, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21TransactionId)), 0) + } + + // create and evaluate a transaction that would otherwise pass + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + time.Sleep(10 * time.Second) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.False(t, pass) +} + +// User onboarded in the last 48 hours and has +// transacted more than 7.5K in the last 90 minutes +func TestEvaluateTransactionNewUserHighSpend(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) + assert.NoError(t, err) + defer db.Close() + + userId, u21UserId, err := createMockUser(mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21UserId)), 0) + // create, evaluate, and execute 6 successful transactions for $1500 + for i := 0; i < 6; i++ { + transaction := createMockTransactionForUser(userId, "1500000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.True(t, pass) + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + u21TransactionId, err := executeMockTransactionForUser(transaction, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21TransactionId)), 0) + } + + // create and evaluate a transaction that would otherwise pass + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) + assetId1 := uuid.NewString() + assetId2 := uuid.NewString() + instrumentId1 := uuid.NewString() + instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + time.Sleep(10 * time.Second) + pass, err := evaluateMockTransaction(transaction, sqlxDB) + assert.NoError(t, err) + assert.False(t, pass) +} + +func evaluateMockTransaction(transaction model.Transaction, sqlxDB *sqlx.DB) (pass bool, err error) { + repo := TransactionRepo{ + TxLeg: repository.NewTxLeg((sqlxDB)), + User: repository.NewUser(sqlxDB), + Asset: repository.NewAsset(sqlxDB), + } + + u21Transaction := NewTransaction(repo) + + pass, err = u21Transaction.Evaluate(transaction) + + return +} diff --git a/pkg/internal/unit21/instrument.go b/pkg/internal/unit21/instrument.go index ae8cef1f..d4b72c7d 100644 --- a/pkg/internal/unit21/instrument.go +++ b/pkg/internal/unit21/instrument.go @@ -174,7 +174,6 @@ func (i instrument) getInstrumentDigitalData(userId string) (digitalData instrum for _, device := range devices { digitalData.IpAddresses = append(digitalData.IpAddresses, device.IpAddresses...) } - log.Printf("deviceData: %s", digitalData) return } @@ -235,7 +234,5 @@ func mapToUnit21Instrument(instrument model.Instrument, source string, entityDat // Options: &options, } - log.Printf("%+v\n", jsonBody) - return jsonBody } diff --git a/pkg/internal/unit21/instrument_test.go b/pkg/internal/unit21/instrument_test.go index 0367a8a6..73f72062 100644 --- a/pkg/internal/unit21/instrument_test.go +++ b/pkg/internal/unit21/instrument_test.go @@ -9,92 +9,38 @@ import ( "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/repository" "github.com/google/uuid" - "github.com/jmoiron/sqlx" - "github.com/joho/godotenv" "github.com/lib/pq" "github.com/stretchr/testify/assert" ) func TestCreateInstrument(t *testing.T) { - err := godotenv.Load("../../../.env") + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } defer db.Close() - instrumentId := uuid.NewString() userId := uuid.NewString() - locationId := uuid.NewString() - instrument := model.Instrument{ - ID: instrumentId, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - DeactivatedAt: nil, - Type: "Credit Card", - Status: "Verified", - Tags: nil, - Network: "Visa", - PublicKey: "", - Last4: "1234", - UserID: userId, - LocationID: sql.NullString{String: locationId}, - } - - mockedUserRow1 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). - AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") - mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(userId).WillReturnRows(mockedUserRow1) - - mockedUserRow2 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). - AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") - mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(userId).WillReturnRows(mockedUserRow2) - - mockedDeviceRow := sqlmock.NewRows([]string{"id", "type", "description", "fingerprint", "ip_addresses", "user_id"}). - AddRow(uuid.NewString(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"187.25.24.128"}, userId) - mock.ExpectQuery("SELECT * FROM device WHERE user_id = $1 LIMIT $2 OFFSET $3").WithArgs(userId, 100, 0).WillReturnRows(mockedDeviceRow) - - mockedLocationRow := sqlmock.NewRows([]string{"id", "type", "status", "building_number", "unit_number", "street_name", "city", "state", "postal_code", "country"}). - AddRow(locationId, "Home", "Verified", "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") - mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(locationId).WillReturnRows(mockedLocationRow) - - repo := InstrumentRepo{ - User: repository.NewUser(sqlxDB), - Device: repository.NewDevice(sqlxDB), - Location: repository.NewLocation(sqlxDB), - } - - u21Instrument := NewInstrument(repo) - - u21InstrumentId, err := u21Instrument.Create(instrument) + _, u21InstrumentId, err := createMockInstrumentForUser(userId, mock, sqlxDB) assert.NoError(t, err) assert.Greater(t, len([]rune(u21InstrumentId)), 0) - //validate response from Unit21 - //check Unit21 dashboard for new instrument added - // TODO: mock call to client once it's manually tested } func TestUpdateInstrument(t *testing.T) { - err := godotenv.Load("../../../.env") + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } defer db.Close() - instrumentId := "4c2b64dd-3471-4869-9099-3882882b47cb" userId := uuid.NewString() + + instrument, u21InstrumentId, err := createMockInstrumentForUser(userId, mock, sqlxDB) + assert.NoError(t, err) + assert.Greater(t, len([]rune(u21InstrumentId)), 0) + locationId := uuid.NewString() - instrument := model.Instrument{ - ID: instrumentId, + instrument = model.Instrument{ + ID: instrument.ID, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeactivatedAt: nil, @@ -110,11 +56,11 @@ func TestUpdateInstrument(t *testing.T) { mockedUserRow1 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") - mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(userId).WillReturnRows(mockedUserRow1) + mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND deactivated_at IS NULL").WithArgs(userId).WillReturnRows(mockedUserRow1) mockedUserRow2 := sqlmock.NewRows([]string{"id", "type", "status", "tags", "first_name", "middle_name", "last_name"}). AddRow(userId, "User", "Onboarded", `{"kyc_level": "1", "platform": "mortal kombat"}`, "Daemon", "", "Targaryan") - mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(userId).WillReturnRows(mockedUserRow2) + mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND deactivated_at IS NULL").WithArgs(userId).WillReturnRows(mockedUserRow2) mockedDeviceRow := sqlmock.NewRows([]string{"id", "type", "description", "fingerprint", "ip_addresses", "user_id"}). AddRow(uuid.NewString(), "Mobile", "iPhone 11S", uuid.NewString(), pq.StringArray{"187.25.24.128"}, userId) @@ -122,7 +68,7 @@ func TestUpdateInstrument(t *testing.T) { mockedLocationRow := sqlmock.NewRows([]string{"id", "type", "status", "building_number", "unit_number", "street_name", "city", "state", "postal_code", "country"}). AddRow(locationId, "Home", "Verified", "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") - mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(locationId).WillReturnRows(mockedLocationRow) + mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND deactivated_at IS NULL").WithArgs(locationId).WillReturnRows(mockedLocationRow) repo := InstrumentRepo{ User: repository.NewUser(sqlxDB), Device: repository.NewDevice(sqlxDB), @@ -131,7 +77,7 @@ func TestUpdateInstrument(t *testing.T) { u21Instrument := NewInstrument(repo) - u21InstrumentId, err := u21Instrument.Update(instrument) + u21InstrumentId, err = u21Instrument.Update(instrument) assert.NoError(t, err) assert.Greater(t, len([]rune(u21InstrumentId)), 0) diff --git a/pkg/internal/unit21/transaction.go b/pkg/internal/unit21/transaction.go index 2125a0bc..1858ff82 100644 --- a/pkg/internal/unit21/transaction.go +++ b/pkg/internal/unit21/transaction.go @@ -2,7 +2,6 @@ package unit21 import ( "encoding/json" - "fmt" "log" "os" @@ -37,24 +36,31 @@ func (t transaction) Evaluate(transaction model.Transaction) (pass bool, err err log.Printf("Failed to gather Unit21 transaction source: %s", err) return false, common.StringError(err) } - url := "https://rtr.sandbox2.unit21.com/evaluate" // will need to be hardcoded for production - body, err := u21Post(url, mapToUnit21Event(transaction, transactionData)) + url := os.Getenv("UNIT21_RTR_URL") + if url == "" { + url = "https://rtr.sandbox2.unit21.com/evaluate" + } + + body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData)) if err != nil { log.Printf("Unit21 Transaction evaluate failed: %s", err) return false, common.StringError(err) } // var u21Response *createEventResponse - var response any + var response evaluateEventResponse err = json.Unmarshal(body, &response) if err != nil { log.Printf("Reading body failed: %s", err) return false, common.StringError(err) } - log.Printf("unit21 evaluate response: %+v", response) - // log.Printf("Unit21Id: %s") + for _, rule := range *response.RuleExecutions { + if rule.Status != "PASS" { + return false, nil + } + } return true, nil } @@ -68,7 +74,7 @@ func (t transaction) Create(transaction model.Transaction) (unit21Id string, err } url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/events/create" - body, err := u21Post(url, mapToUnit21Event(transaction, transactionData)) + body, err := u21Post(url, mapToUnit21TransactionEvent(transaction, transactionData)) if err != nil { log.Printf("Unit21 Transaction create failed: %s", err) return "", common.StringError(err) @@ -95,7 +101,7 @@ func (t transaction) Update(transaction model.Transaction) (unit21Id string, err orgName := os.Getenv("UNIT21_ORG_NAME") url := "https://" + os.Getenv("UNIT21_ENV") + ".unit21.com/v1/" + orgName + "/events/" + transaction.ID + "/update" - body, err := u21Put(url, mapToUnit21Event(transaction, transactionData)) + body, err := u21Put(url, mapToUnit21TransactionEvent(transaction, transactionData)) if err != nil { log.Printf("Unit21 Transaction create failed: %s", err) @@ -162,24 +168,30 @@ func (t transaction) getTransactionData(transaction model.Transaction) (txData t err = common.StringError(err) return } - - stringFee, err := common.BigNumberToFloat(transaction.StringFee, 6) - if err != nil { - log.Printf("Failed to convert stringFee: %s", err) - err = common.StringError(err) - return + var stringFee float64 + if transaction.StringFee != "" { + stringFee, err = common.BigNumberToFloat(transaction.StringFee, 6) + if err != nil { + log.Printf("Failed to convert stringFee: %s", err) + err = common.StringError(err) + return + } } - processingFee, err := common.BigNumberToFloat(transaction.ProcessingFee, 6) - if err != nil { - log.Printf("Failed to convert processingFee: %s", err) - err = common.StringError(err) - return + var processingFee float64 + if transaction.ProcessingFee != "" { + processingFee, err = common.BigNumberToFloat(transaction.ProcessingFee, 6) + if err != nil { + log.Printf("Failed to convert processingFee: %s", err) + err = common.StringError(err) + return + } } - fmt.Printf("senderAsset: %+v\n", senderAsset) - log.Printf("senderAsset.Name: %s", senderAsset.Name) - log.Printf("receiverAsset.Name: %s", receiverAsset.Name) + var exchangeRate float64 + if receiverAmount > 0 { + exchangeRate = senderAmount / receiverAmount + } txData = transactionData{ Amount: amount, @@ -193,7 +205,7 @@ func (t transaction) getTransactionData(transaction model.Transaction) (txData t ReceiverEntityId: receiverData.UserID, ReceiverEntityType: "user", ReceiverInstrumentId: receiverData.InstrumentID, - ExchangeRate: senderAmount / receiverAmount, + ExchangeRate: exchangeRate, TransactionHash: transaction.TransactionHash, USDConversionNotes: "", InternalFee: stringFee, @@ -203,7 +215,7 @@ func (t transaction) getTransactionData(transaction model.Transaction) (txData t return } -func mapToUnit21Event(transaction model.Transaction, transactionData transactionData) *u21Event { +func mapToUnit21TransactionEvent(transaction model.Transaction, transactionData transactionData) *u21Event { var transactionTagArr []string if transaction.Tags != nil { for key, value := range transaction.Tags { @@ -213,10 +225,10 @@ func mapToUnit21Event(transaction model.Transaction, transactionData transaction jsonBody := &u21Event{ GeneralData: &eventGeneral{ - EventId: transaction.ID, - EventType: "transaction", - EventTime: int(transaction.CreatedAt.Unix()), - EventSubtype: "", + EventId: transaction.ID, //required + EventType: "transaction", //required + EventTime: int(transaction.CreatedAt.Unix()), //required + EventSubtype: "credit_card", //required for RTR Status: transaction.Status, Parents: nil, Tags: transactionTagArr, diff --git a/pkg/internal/unit21/transaction_test.go b/pkg/internal/unit21/transaction_test.go index 2563b62f..ed4845d9 100644 --- a/pkg/internal/unit21/transaction_test.go +++ b/pkg/internal/unit21/transaction_test.go @@ -1,6 +1,7 @@ package unit21 import ( + "database/sql" "testing" "time" @@ -9,115 +10,56 @@ import ( "github.com/String-xyz/string-api/pkg/repository" "github.com/google/uuid" "github.com/jmoiron/sqlx" - "github.com/joho/godotenv" "github.com/lib/pq" "github.com/stretchr/testify/assert" ) -func TestEvaluateTransaction(t *testing.T) { - err := godotenv.Load("../../../.env") +func TestCreateTransaction(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } defer db.Close() - transactionId := uuid.NewString() - OriginTxLegID := uuid.NewString() - DestinationTxLegID := uuid.NewString() - userId1 := uuid.NewString() - userId2 := uuid.NewString() + userId := uuid.NewString() + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) assetId1 := uuid.NewString() assetId2 := uuid.NewString() - networkId := uuid.NewString() instrumentId1 := uuid.NewString() instrumentId2 := uuid.NewString() - - transaction := model.Transaction{ - ID: transactionId, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Type: "fiat-to-crypto", - Status: "Completed", - Tags: map[string]string{}, - DeviceID: uuid.NewString(), - IPAddress: "187.25.24.128", - PlatformID: uuid.NewString(), - TransactionHash: "", - NetworkID: networkId, - NetworkFee: "100000000", - ContractParams: pq.StringArray{}, - ContractFunc: "mintTo()", - TransactionAmount: "1000000000", - OriginTxLegID: OriginTxLegID, - ReceiptTxLegID: uuid.NewString(), - ResponseTxLegID: uuid.NewString(), - DestinationTxLegID: DestinationTxLegID, - ProcessingFee: "1000000", - ProcessingFeeAsset: uuid.NewString(), - StringFee: "1000000", - } - - mockedTxLegRow1 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(OriginTxLegID, time.Now(), "1000000", "1000000", assetId1, userId1, instrumentId1) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(OriginTxLegID).WillReturnRows(mockedTxLegRow1) - - mockedTxLegRow2 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(DestinationTxLegID, time.Now(), "1", "10000000", assetId2, userId2, instrumentId2) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(DestinationTxLegID).WillReturnRows(mockedTxLegRow2) - - mockedAssetRow1 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId1, "USD", "fiat USD", 6, false, networkId, "self") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId1).WillReturnRows(mockedAssetRow1) - - mockedAssetRow2 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId2, "Noose The Goose", "Noose the Goose NFT", 0, true, networkId, "joepegs.com") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId2).WillReturnRows(mockedAssetRow2) - - repo := TransactionRepo{ - TxLeg: repository.NewTxLeg((sqlxDB)), - User: repository.NewUser(sqlxDB), - Asset: repository.NewAsset(sqlxDB), - } - - u21Transaction := NewTransaction(repo) - - pass, err := u21Transaction.Evaluate(transaction) + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + u21TransactionId, err := executeMockTransactionForUser(transaction, sqlxDB) assert.NoError(t, err) - assert.True(t, pass) + assert.Greater(t, len([]rune(u21TransactionId)), 0) //validate response from Unit21 //check Unit21 dashboard for new transaction added // TODO: mock call to client once it's manually tested } -func TestCreateTransaction(t *testing.T) { - err := godotenv.Load("../../../.env") +func TestUpdateTransaction(t *testing.T) { + db, mock, sqlxDB, err := initializeTest(t) assert.NoError(t, err) - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) - } defer db.Close() - transactionId := uuid.NewString() - OriginTxLegID := uuid.NewString() - DestinationTxLegID := uuid.NewString() - userId1 := uuid.NewString() - userId2 := uuid.NewString() + userId := uuid.NewString() + transaction := createMockTransactionForUser(userId, "1000000", sqlxDB) assetId1 := uuid.NewString() assetId2 := uuid.NewString() - networkId := uuid.NewString() instrumentId1 := uuid.NewString() instrumentId2 := uuid.NewString() + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) + u21TransactionId, err := executeMockTransactionForUser(transaction, sqlxDB) + assert.NoError(t, err) - transaction := model.Transaction{ - ID: transactionId, + OriginTxLegID := uuid.NewString() + DestinationTxLegID := uuid.NewString() + assetId1 = uuid.NewString() + assetId2 = uuid.NewString() + networkId := uuid.NewString() + instrumentId1 = uuid.NewString() + instrumentId2 = uuid.NewString() + + transaction = model.Transaction{ + ID: transaction.ID, CreatedAt: time.Now(), UpdatedAt: time.Now(), Type: "fiat-to-crypto", @@ -133,39 +75,24 @@ func TestCreateTransaction(t *testing.T) { ContractFunc: "mintTo()", TransactionAmount: "1000000000", OriginTxLegID: OriginTxLegID, - ReceiptTxLegID: uuid.NewString(), - ResponseTxLegID: uuid.NewString(), + ReceiptTxLegID: sql.NullString{String: uuid.NewString()}, + ResponseTxLegID: sql.NullString{String: uuid.NewString()}, DestinationTxLegID: DestinationTxLegID, ProcessingFee: "1000000", ProcessingFeeAsset: uuid.NewString(), - StringFee: "1000000", + StringFee: "2000000", } - - mockedTxLegRow1 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(OriginTxLegID, time.Now(), "1000000", "1000000", assetId1, userId1, instrumentId1) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(OriginTxLegID).WillReturnRows(mockedTxLegRow1) - - mockedTxLegRow2 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(DestinationTxLegID, time.Now(), "1", "10000000", assetId2, userId2, instrumentId2) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(DestinationTxLegID).WillReturnRows(mockedTxLegRow2) - - mockedAssetRow1 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId1, "USD", "fiat USD", 6, false, networkId, "self") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId1).WillReturnRows(mockedAssetRow1) - - mockedAssetRow2 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId2, "Noose The Goose", "Noose the Goose NFT", 0, true, networkId, "joepegs.com") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId2).WillReturnRows(mockedAssetRow2) + mockTransactionRows(mock, transaction, userId, assetId1, assetId2, instrumentId1, instrumentId2) repo := TransactionRepo{ - TxLeg: repository.NewTxLeg(sqlxDB), + TxLeg: repository.NewTxLeg((sqlxDB)), User: repository.NewUser(sqlxDB), Asset: repository.NewAsset(sqlxDB), } u21Transaction := NewTransaction(repo) - u21TransactionId, err := u21Transaction.Create(transaction) + u21TransactionId, err = u21Transaction.Update(transaction) assert.NoError(t, err) assert.Greater(t, len([]rune(u21TransactionId)), 0) @@ -174,29 +101,27 @@ func TestCreateTransaction(t *testing.T) { // TODO: mock call to client once it's manually tested } -func TestUpdateTransaction(t *testing.T) { - err := godotenv.Load("../../../.env") - assert.NoError(t, err) - - db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - sqlxDB := sqlx.NewDb(db, "sqlmock") - if err != nil { - t.Fatalf("error %s was not expected when opening stub db", err) +func executeMockTransactionForUser(transaction model.Transaction, sqlxDB *sqlx.DB) (unit21Id string, err error) { + repo := TransactionRepo{ + TxLeg: repository.NewTxLeg(sqlxDB), + User: repository.NewUser(sqlxDB), + Asset: repository.NewAsset(sqlxDB), } - defer db.Close() - transactionId := "866c32ae-9fda-409c-a0be-28b830022e93" + u21Transaction := NewTransaction(repo) + + unit21Id, err = u21Transaction.Create(transaction) + + return +} + +func createMockTransactionForUser(userId string, amount string, sqlxDB *sqlx.DB) (transaction model.Transaction) { + transactionId := uuid.NewString() OriginTxLegID := uuid.NewString() DestinationTxLegID := uuid.NewString() - userId1 := uuid.NewString() - userId2 := uuid.NewString() - assetId1 := uuid.NewString() - assetId2 := uuid.NewString() networkId := uuid.NewString() - instrumentId1 := uuid.NewString() - instrumentId2 := uuid.NewString() - transaction := model.Transaction{ + transaction = model.Transaction{ ID: transactionId, CreatedAt: time.Now(), UpdatedAt: time.Now(), @@ -211,45 +136,33 @@ func TestUpdateTransaction(t *testing.T) { NetworkFee: "100000000", ContractParams: pq.StringArray{}, ContractFunc: "mintTo()", - TransactionAmount: "1000000000", + TransactionAmount: amount, OriginTxLegID: OriginTxLegID, - ReceiptTxLegID: uuid.NewString(), - ResponseTxLegID: uuid.NewString(), + ReceiptTxLegID: sql.NullString{String: uuid.NewString()}, + ResponseTxLegID: sql.NullString{String: uuid.NewString()}, DestinationTxLegID: DestinationTxLegID, ProcessingFee: "1000000", ProcessingFeeAsset: uuid.NewString(), - StringFee: "2000000", + StringFee: "1000000", } + return +} + +func mockTransactionRows(mock sqlmock.Sqlmock, transaction model.Transaction, userId string, assetId1 string, assetId2 string, instrumentId1 string, instrumentId2 string) { mockedTxLegRow1 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(OriginTxLegID, time.Now(), "1000000", "1000000", assetId1, userId1, instrumentId1) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(OriginTxLegID).WillReturnRows(mockedTxLegRow1) + AddRow(transaction.OriginTxLegID, time.Now(), transaction.TransactionAmount, transaction.TransactionAmount, assetId1, userId, instrumentId1) + mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND deactivated_at IS NULL").WithArgs(transaction.OriginTxLegID).WillReturnRows(mockedTxLegRow1) mockedTxLegRow2 := sqlmock.NewRows([]string{"id", "timestamp", "amount", "value", "asset_id", "user_id", "instrument_id"}). - AddRow(DestinationTxLegID, time.Now(), "1", "10000000", assetId2, userId2, instrumentId2) - mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(DestinationTxLegID).WillReturnRows(mockedTxLegRow2) + AddRow(transaction.DestinationTxLegID, time.Now(), "1", transaction.TransactionAmount, assetId2, userId, instrumentId2) + mock.ExpectQuery("SELECT * FROM tx_leg WHERE id = $1 AND deactivated_at IS NULL").WithArgs(transaction.DestinationTxLegID).WillReturnRows(mockedTxLegRow2) mockedAssetRow1 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId1, "USD", "fiat USD", 6, false, networkId, "self") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId1).WillReturnRows(mockedAssetRow1) + AddRow(assetId1, "USD", "fiat USD", 6, false, transaction.NetworkID, "self") + mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND deactivated_at IS NULL").WithArgs(assetId1).WillReturnRows(mockedAssetRow1) mockedAssetRow2 := sqlmock.NewRows([]string{"id", "name", "description", "decimals", "is_crypto", "network_id", "value_oracle"}). - AddRow(assetId2, "Noose The Goose", "Noose the Goose NFT", 0, true, networkId, "joepegs.com") - mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WithArgs(assetId2).WillReturnRows(mockedAssetRow2) - - repo := TransactionRepo{ - TxLeg: repository.NewTxLeg((sqlxDB)), - User: repository.NewUser(sqlxDB), - Asset: repository.NewAsset(sqlxDB), - } - - u21Transaction := NewTransaction(repo) - - u21TransactionId, err := u21Transaction.Update(transaction) - assert.NoError(t, err) - assert.Greater(t, len([]rune(u21TransactionId)), 0) - - //validate response from Unit21 - //check Unit21 dashboard for new transaction added - // TODO: mock call to client once it's manually tested + AddRow(assetId2, "Noose The Goose", "Noose the Goose NFT", 0, true, transaction.NetworkID, "joepegs.com") + mock.ExpectQuery("SELECT * FROM asset WHERE id = $1 AND deactivated_at IS NULL").WithArgs(assetId2).WillReturnRows(mockedAssetRow2) } diff --git a/pkg/internal/unit21/types.go b/pkg/internal/unit21/types.go index 5878d731..072e2d9b 100644 --- a/pkg/internal/unit21/types.go +++ b/pkg/internal/unit21/types.go @@ -125,7 +125,7 @@ type updateInstrumentResponse struct { type u21Event struct { GeneralData *eventGeneral `json:"general_data"` - TransactionData *transactionData `json:"transaction_data"` + TransactionData *transactionData `json:"transaction_data,omitempty"` ActionData *actionData `json:"action_data,omitempty"` DigitalData *eventDigitalData `json:"digital_data,omitempty"` LocationData *instrumentLocationData `json:"location_data,omitempty"` @@ -194,3 +194,19 @@ type updateEventResponse struct { EventId string `json:"event_id"` Unit21Id string `json:"unit21_id"` } + +type evaluateEventResponse struct { + Endpoint string `json:"endpoint"` + EvaluationId string `json:"evaluation_id"` + EventId string `json:"event_id"` + OrgId int `json:"org_id"` + RuleExecutions *ruleExecutions `json:"rule_executions"` + Timestamp float64 `json:"timestamp"` +} + +type ruleExecutions map[string]rule + +type rule struct { + RuleName string `json:"rule_name"` + Status string `json:"status"` +} diff --git a/pkg/model/entity.go b/pkg/model/entity.go index edce1d9a..bc8c0027 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -167,25 +167,25 @@ type Transaction struct { CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Type string `json:"type" db:"type"` - Status string `json:"status" db:"status"` - Tags StringMap `json:"tags" db:"tags"` - DeviceID string `json:"deviceId" db:"device_id"` - IPAddress string `json:"ipAddress" db:"ip_address"` - PlatformID string `json:"platformId" db:"platform_id"` - TransactionHash string `json:"transactionHash" db:"transaction_hash"` - NetworkID string `json:"networkId" db:"network_id"` - NetworkFee string `json:"networkFee" db:"network_fee"` - ContractParams pq.StringArray `json:"contractParameters" db:"contract_params"` - ContractFunc string `json:"contractFunc" db:"contract_func"` - TransactionAmount string `json:"transactionAmount" db:"transaction_amount"` - OriginTxLegID string `json:"originTxLegId" db:"origin_tx_leg_id"` - ReceiptTxLegID string `json:"receiptTxLegId" db:"receipt_tx_leg_id"` - ResponseTxLegID string `json:"responseTxLegId" db:"response_tx_leg_id"` - DestinationTxLegID string `json:"destinationTxLegId" db:"destination_tx_leg_id"` - ProcessingFee string `json:"processingFee" db:"processing_fee"` - ProcessingFeeAsset string `json:"processingFeeAsset" db:"processing_fee_asset"` - StringFee string `json:"stringFee" db:"string_fee"` + Type string `json:"type,omitempty" db:"type"` + Status string `json:"status,omitempty" db:"status"` + Tags StringMap `json:"tags,omitempty" db:"tags"` + DeviceID string `json:"deviceId,omitempty" db:"device_id"` + IPAddress string `json:"ipAddress,omitempty" db:"ip_address"` + PlatformID string `json:"platformId,omitempty" db:"platform_id"` + TransactionHash string `json:"transactionHash,omitempty" db:"transaction_hash"` + NetworkID string `json:"networkId,omitempty" db:"network_id"` + NetworkFee string `json:"networkFee,omitempty" db:"network_fee"` + ContractParams pq.StringArray `json:"contractParameters,omitempty" db:"contract_params"` + ContractFunc string `json:"contractFunc,omitempty" db:"contract_func"` + TransactionAmount string `json:"transactionAmount,omitempty" db:"transaction_amount"` + OriginTxLegID string `json:"originTxLegId,omitempty" db:"origin_tx_leg_id"` + ReceiptTxLegID sql.NullString `json:"receiptTxLegId,omitempty" db:"receipt_tx_leg_id"` + ResponseTxLegID sql.NullString `json:"responseTxLegId,omitempty" db:"response_tx_leg_id"` + DestinationTxLegID string `json:"destinationTxLegId,omitempty" db:"destination_tx_leg_id"` + ProcessingFee string `json:"processingFee,omitempty" db:"processing_fee"` + ProcessingFeeAsset string `json:"processingFeeAsset,omitempty" db:"processing_fee_asset"` + StringFee string `json:"stringFee,omitempty" db:"string_fee"` } type AuthStrategy struct { diff --git a/pkg/model/request.go b/pkg/model/request.go index 080b2d10..9f3ff64a 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -32,6 +32,15 @@ type TransactionUpdates struct { StringFee *string `json:"stringFee" db:"string_fee"` } +type TxLegUpdates struct { + Timestamp *time.Time `json:"timestamp" db:"timestamp"` + Amount *string `json:"amount" db:"amount"` + Value *string `json:"value" db:"value"` + AssetID *string `json:"assetId" db:"asset_id"` + UserID *string `json:"userId" db:"user_id"` + InstrumentID *string `json:"instrumentId" db:"instrument_id"` +} + type UserRegister struct { FirstName string `json:"firstName" db:"first_name"` MiddleName string `json:"middleName" db:"middle_name"` diff --git a/pkg/repository/location_test.go b/pkg/repository/location_test.go index d9ae37de..b7c4291a 100644 --- a/pkg/repository/location_test.go +++ b/pkg/repository/location_test.go @@ -22,7 +22,7 @@ func TestGetLocation(t *testing.T) { rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "type", "status", "building_number", "unit_number", "street_name", "city", "state", "postal_code", "country"}). AddRow(id, time.Now(), time.Now(), "Home", "Verified", "20181", "411", "Lark Avenue", "Somerville", "MA", "01443", "USA") - mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WillReturnRows(rows).WithArgs(id) + mock.ExpectQuery("SELECT * FROM location WHERE id = $1 AND deactivated_at IS NULL").WillReturnRows(rows).WithArgs(id) location, err := NewLocation(sqlxDB).GetById(id) assert.NoError(t, err) diff --git a/pkg/repository/user_test.go b/pkg/repository/user_test.go index 578f48c8..4625c194 100644 --- a/pkg/repository/user_test.go +++ b/pkg/repository/user_test.go @@ -44,7 +44,7 @@ func TestGetUser(t *testing.T) { rows := sqlmock.NewRows([]string{"id", "first_name", "last_name", "created_at", "updated_at"}). AddRow(id, "Mocking", "Jay", time.Now(), time.Now()) - mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WillReturnRows(rows).WithArgs(id) + mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND deactivated_at IS NULL").WillReturnRows(rows).WithArgs(id) user, err := NewUser(sqlxDB).GetById(id) assert.NoError(t, err) diff --git a/pkg/service/chain.go b/pkg/service/chain.go index fa1f720b..4e1fd2df 100644 --- a/pkg/service/chain.go +++ b/pkg/service/chain.go @@ -15,6 +15,7 @@ type Chain struct { OwlracleName string StringFee float64 UUID string + GasTokenID string } // TODO: should we store this in a DB or determine it dynamically??? Previously this was defined in the preprocessor in the Chain array @@ -35,5 +36,5 @@ func ChainInfo(chainId uint64, networkRepo repository.Network, assetRepo reposit if err != nil { return Chain{}, common.StringError(err) } - return Chain{ChainID: chainId, RPC: network.RPCUrl, Explorer: network.ExplorerUrl, CoingeckoName: asset.ValueOracle.String, OwlracleName: network.GasOracle, StringFee: fee, UUID: network.ID}, nil + return Chain{ChainID: chainId, RPC: network.RPCUrl, Explorer: network.ExplorerUrl, CoingeckoName: asset.ValueOracle.String, OwlracleName: network.GasOracle, StringFee: fee, UUID: network.ID, GasTokenID: network.GasTokenID}, nil } diff --git a/pkg/service/checkout.go b/pkg/service/checkout.go index 24cf9834..916a9bd2 100644 --- a/pkg/service/checkout.go +++ b/pkg/service/checkout.go @@ -53,6 +53,10 @@ type AuthorizedCharge struct { CheckoutFingerprint string Last4 string Issuer string + Approved bool + Status string + Summary string + CardType string } func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth AuthorizedCharge, err error) { @@ -67,8 +71,11 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au // Generate a payment token ID in case we don't yet have one in the front end // For testing purposes only card := tokens.Card{ - Type: checkoutCommon.Card, - Number: "4242424242424242", + Type: checkoutCommon.Card, + Number: "4242424242424242", // Success + // Number: "4273149019799094", // succeed authorize, fail capture + // Number: "4544249167673670", // Declined - Insufficient funds + // Number: "5148447461737269", // Invalid transaction ExpiryMonth: 2, ExpiryYear: 2024, Name: "Customer Name", @@ -107,18 +114,25 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au IdempotencyKey: &idempotencyKey, } response, err := client.Request(request, ¶ms) - if err != nil { return auth, common.StringError(err) } // Collect authorization ID and Instrument ID - auth.AuthID = response.Processed.ID - if response.Processed.Source.CardSourceResponse != nil { - auth.Last4 = response.Processed.Source.CardSourceResponse.Last4 - auth.Issuer = response.Processed.Source.Issuer - auth.CheckoutFingerprint = response.Processed.Source.CardSourceResponse.Fingerprint + if response.Processed != nil { + auth.AuthID = response.Processed.ID + auth.Approved = *response.Processed.Approved + auth.Status = string(response.Processed.Status) + auth.Summary = response.Processed.ResponseSummary + auth.CardType = string(response.Processed.Source.CardType) + + if response.Processed.Source.CardSourceResponse != nil { + auth.Last4 = response.Processed.Source.CardSourceResponse.Last4 + auth.Issuer = response.Processed.Source.Issuer + auth.CheckoutFingerprint = response.Processed.Source.CardSourceResponse.Fingerprint + } } + // TODO: Create entry for authorization in our DB associated with userWallet return auth, nil } @@ -139,6 +153,7 @@ func CaptureCharge(amount float64, userWallet string, authorizationID string) (c request := payments.CapturesRequest{ Amount: usd, } + capture, err = client.Captures(authorizationID, &request, ¶ms) if err != nil { return nil, common.StringError(err) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 76ab6e7d..f305a5b3 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -177,23 +177,77 @@ func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId s if err != nil { return res, common.StringError(err) } - status = "Card Authorized" + + status = "Card " + cardAuthorization.Status updateDB.Status = &status err = t.repos.Transaction.Update(db.ID, updateDB) if err != nil { return res, common.StringError(err) } - // // Turning off until we can determine the Destination Leg prior to execution - // u21auth, err := t.unit21Evaluate(db.ID) - // if err != nil { - // return res, common.StringError(err) - // } + recipientWalletId, err := t.addWalletInstrumentIdIfNew(e.UserAddress, user.ID) + if err != nil { + return res, common.StringError(err) + } + + // TODO: Determine the output of the transaction (destination leg) with Tracers + destinationLeg := model.TxLeg{ + Timestamp: time.Now(), // Required by the db. Should be updated when the tx occurs + Amount: "0", // Required by Unit21. The amount of the asset received by the user + Value: "0", // Default to '0'. The value of the asset received by the user + AssetID: chain.GasTokenID, // Required by the db. the asset received by the user + UserID: user.ID, // the user who received the asset + InstrumentID: recipientWalletId, // Required by the db. the instrument which received the asset (wallet usually) + } + + destinationLeg, err = t.repos.TxLeg.Create(destinationLeg) + if err != nil { + return res, common.StringError(err) + } + + txLeg := model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID} + + err = t.repos.Transaction.Update(db.ID, txLeg) + if err != nil { + return res, common.StringError(err) + } + + if !cardAuthorization.Approved { + err := t.unit21CreateTransaction(db.ID) + if err != nil { + return res, common.StringError(err) + } + + return res, common.StringError(common.StringError(errors.New("payment: Authorization Declined by Checkout"))) + } + + // Validate Transaction through Real Time Rules engine + u21auth, err := t.unit21Evaluate(db.ID) + if err != nil { + return res, common.StringError(err) + } + + if !u21auth { + status = "Failed" + updateDB.Status = &status + err = t.repos.Transaction.Update(db.ID, updateDB) + if err != nil { + return res, common.StringError(err) + } - // if !u21auth { - // err = fmt.Errorf("Transaction Unauthorized in Unit21") - // return res, common.StringError(err) - // } + err = t.unit21CreateTransaction(db.ID) + if err != nil { + return res, common.StringError(err) + } + + return res, common.StringError(errors.New("risk: Transaction Failed Unit21 Real Time Rules Evaluation")) + } + status = "Unit21 Authorized" + updateDB.Status = &status + err = t.repos.Transaction.Update(db.ID, updateDB) + if err != nil { + return res, common.StringError(err) + } // Send request to the blockchain and update model status, hash, transaction amount txID, value, err := t.initiateTransaction(executor, e, processingFeeAsset, db.ID, userId) @@ -225,6 +279,7 @@ func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId s processingFeeAsset: processingFeeAsset, preBalance: preBalance, userId: userId, + recipientWalletId: recipientWalletId, } go t.postProcess(post) @@ -294,26 +349,6 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio TokenName: "", } - // // TODO: Determine the output of the transaction! - // // We need to determine the DestinationTXLeg here - // destinationLeg := model.TxLeg{ - // Timestamp: time.Now(), // null? Should be updated when the tx occurs - // Amount: wei, // Should be the amount of the asset received by the user - // Value: usd, // The value of the asset received by the user - // AssetID: asset.ID, // the asset received by the user - // UserID: recipientId, // the user who received the asset - // InstrumentID: userWalletId, // the instrument which received the asset (wallet usually) - // } - // destinationLeg, err = t.repos.TxLeg.Create(destinationLeg) - // if err != nil { - // return res, eth, common.StringError(err) - // } - // txLeg := model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID} - // err = t.repos.Transaction.Update(txUUID, txLeg) - // if err != nil { - // return res, eth, common.StringError(err) - // } - // Estimate Cost in USD to execute Tx request estimateUSD, err := cost.EstimateTransaction(estimationParams, chain) if err != nil { @@ -348,7 +383,7 @@ func verifyQuote(e model.ExecutionRequest, newEstimate model.Quote) (bool, error return true, nil } -func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, last4 string) (string, error) { +func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, last4 string, cardType string) (string, error) { instrument, err := t.repos.Instrument.GetWallet(fingerprint) // temporarily using get wallet and storing it there if err != nil && !strings.Contains(err.Error(), "not found") { // because we are wrapping error and care about its value return "", common.StringError(err) @@ -356,12 +391,18 @@ func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, return instrument.ID, nil // instrument already exists } + // We should gather type from the payment processor + instrument_type := "DebitCard" + if cardType == "CREDIT" { + instrument_type = "CreditCard" + } // Create a new instrument - instrument = model.Instrument{Type: "card", Status: "authorized", Last4: last4, UserID: userID, PublicKey: fingerprint} // No locationID until fingerprint + instrument = model.Instrument{Type: instrument_type, Status: "authorized", Last4: last4, UserID: userID, PublicKey: fingerprint} // No locationID until fingerprint instrument, err = t.repos.Instrument.Create(instrument) if err != nil { return "", common.StringError(err) } + go t.unit21CreateInstrument(instrument) return instrument.ID, nil } @@ -374,11 +415,12 @@ func (t transaction) addWalletInstrumentIdIfNew(address string, id string) (stri } // Create a new instrument - instrument = model.Instrument{Type: "crypto-wallet", Status: "external", Network: "ethereum", PublicKey: address, UserID: id} // No locationID or userID because this wallet was not registered with the user and is some other recipient + instrument = model.Instrument{Type: "CryptoWallet", Status: "external", Network: "ethereum", PublicKey: address, UserID: id} // No locationID or userID because this wallet was not registered with the user and is some other recipient instrument, err = t.repos.Instrument.Create(instrument) if err != nil { return "", common.StringError(err) } + go t.unit21CreateInstrument(instrument) return instrument.ID, nil } @@ -390,7 +432,7 @@ func (t transaction) authCard(userWallet string, cardToken string, usd float64, } // Add Checkout Instrument ID to our DB if it's not there already and associate it with the user - instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, userId, auth.Last4) + instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, userId, auth.Last4, auth.CardType) if err != nil { return auth, common.StringError(err) } @@ -415,8 +457,6 @@ func (t transaction) authCard(userWallet string, cardToken string, usd float64, return auth, common.StringError(err) } - go t.unit21CreateInstrument(origin.InstrumentID) - return auth, nil } @@ -514,20 +554,24 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u } wei := floatToFixedString(trueEth, int(asset.Decimals)) usd := floatToFixedString(quotedTotal, 6) - destinationLeg := model.TxLeg{ - Timestamp: time.Now(), // updated based on *when the transaction occured* not time.Now() - Amount: wei, // Should be the amount of the asset received by the user - Value: usd, // The value of the asset received by the user - AssetID: asset.ID, // the asset received by the user - UserID: recipientId, // the user who received the asset - InstrumentID: userWalletId, // the instrument which received the asset (wallet usually) - } - destinationLeg, err = t.repos.TxLeg.Create(destinationLeg) + + txModel, err := t.repos.Transaction.GetById(txUUID) if err != nil { return profit, common.StringError(err) } - txLeg := model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID} - err = t.repos.Transaction.Update(txUUID, txLeg) + + now := time.Now() + destinationLeg := model.TxLegUpdates{ + Timestamp: &now, // updated based on *when the transaction occured* not time.Now() + Amount: &wei, // Should be the amount of the asset received by the user + Value: &usd, // The value of the asset received by the user + AssetID: &asset.ID, // the asset received by the user + UserID: &recipientId, // the user who received the asset + InstrumentID: &userWalletId, // the instrument which received the asset (wallet usually) + } + + // We now update the destination leg instead of creating it + err = t.repos.TxLeg.Update(txModel.DestinationTxLegID, destinationLeg) if err != nil { return profit, common.StringError(err) } @@ -547,6 +591,7 @@ type postProcessRequest struct { processingFeeAsset model.Asset preBalance float64 userId string + recipientWalletId string } func (t transaction) postProcess(request postProcessRequest) { @@ -594,11 +639,7 @@ func (t transaction) postProcess(request postProcessRequest) { // compute profit, update db status and processing fees to db // TODO: factor request.processingFeeAsset in the event of crypto-to-usd - recipientWalletId, err := t.addWalletInstrumentIdIfNew(request.UserAddress, request.userId) - if err != nil { - // TODO: handle error instead of returning it - } - profit, err := t.tenderTransaction(request.CumulativeValue, trueGas, request.Quote.TotalUSD, request.Chain, request.TxDBID, request.userId, recipientWalletId) + profit, err := t.tenderTransaction(request.CumulativeValue, trueGas, request.Quote.TotalUSD, request.Chain, request.TxDBID, request.userId, request.recipientWalletId) if err != nil { // TODO: Handle error instead of returning it } @@ -636,23 +677,10 @@ func (t transaction) postProcess(request postProcessRequest) { } executor.Close() // Create Transaction data in Unit21 - txModel, err := t.repos.Transaction.GetById(request.TxDBID) - if err != nil { - log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", err) - // return res, common.StringError(err) - } - - u21Repo := unit21.TransactionRepo{ - TxLeg: t.repos.TxLeg, - User: t.repos.User, - Asset: t.repos.Asset, - } - u21Tx := unit21.NewTransaction(u21Repo) - _, err = u21Tx.Create(txModel) + err = t.unit21CreateTransaction(request.TxDBID) if err != nil { - log.Printf("Error updating Unit21 in Tx Postprocess: %s", err) - // return res, common.StringError(err) + log.Printf("Error creating Unit21 transaction: %s", err) } // send email receipt @@ -709,25 +737,58 @@ func floatToFixedString(value float64, decimals int) string { return strconv.FormatUint(uint64(value*(math.Pow10(decimals-1))), 10) } -func (t transaction) unit21CreateInstrument(instrumentId string) { - // Send Instrument Data to Unit21 - instrument, err := t.repos.Instrument.GetById(instrumentId) +func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err error) { + u21InstrumentRepo := unit21.InstrumentRepo{ + User: t.repos.User, + Device: t.repos.Device, + Location: t.repos.Location, // empty until fingerprint integration + } + + u21Instrument := unit21.NewInstrument(u21InstrumentRepo) + u21InstrumentId, err := u21Instrument.Create(instrument) if err != nil { - fmt.Printf("Error creating new instrument in Unit21 -- can't get instrument model") + fmt.Printf("Error creating new instrument in Unit21") return } - u21Repo := unit21.InstrumentRepo{ + // Log create instrument action w/ Unit21 + u21ActionRepo := unit21.ActionRepo{ User: t.repos.User, Device: t.repos.Device, Location: t.repos.Location, // empty until fingerprint integration } - u21Tx := unit21.NewInstrument(u21Repo) - _, err = u21Tx.Create(instrument) + u21Action := unit21.NewAction(u21ActionRepo) + _, err = u21Action.Create(instrument, "Creation", u21InstrumentId, "Creation") if err != nil { - fmt.Printf("Error creating new instrument in Unit21") + fmt.Printf("Error creating a new instrument action in Unit21") + return + } + + return +} + +func (t transaction) unit21CreateTransaction(transactionId string) (err error) { + txModel, err := t.repos.Transaction.GetById(transactionId) + if err != nil { + log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", err) + return } + + u21Repo := unit21.TransactionRepo{ + TxLeg: t.repos.TxLeg, + User: t.repos.User, + Asset: t.repos.Asset, + } + + u21Tx := unit21.NewTransaction(u21Repo) + _, err = u21Tx.Create(txModel) + if err != nil { + log.Printf("Error updating Unit21 in Tx Postprocess: %s", err) + return + } + + return } func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err error) { @@ -735,7 +796,7 @@ func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err txModel, err := t.repos.Transaction.GetById(transactionId) if err != nil { log.Printf("Error getting tx model in Unit21 in Tx Evaluate: %s", err) - return + return evaluation, common.StringError(err) } u21Repo := unit21.TransactionRepo{ @@ -748,7 +809,7 @@ func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err evaluation, err = u21Tx.Evaluate(txModel) if err != nil { log.Printf("Error evaluating transaction in Unit21: %s", err) - return + return evaluation, common.StringError(err) } return From bf7749e072dc64cd5dd705371a53090f6164156f Mon Sep 17 00:00:00 2001 From: akfoster Date: Mon, 6 Feb 2023 12:39:51 -0600 Subject: [PATCH 28/35] update invite table (#107) * update invite table * normalize order --- ...tform_member-role_member-to-role_member-invite_apikey.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index 0af57b6c..b1fd169b 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -51,9 +51,10 @@ CREATE TABLE member_invite ( expired_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, - invited_by UUID REFERENCES platform_member (id), + name TEXT DEFAULT '', + invited_by UUID REFERENCES platform_member (id) DEFAULT NULL, platform_id UUID REFERENCES platform (id), - name TEXT DEFAULT '' + role_id UUID REFERENCES member_role (id) ); ------------------------------------------------------------------------- From ba94e03121d9ecb6f7bfa2ae481ff66ef2822864 Mon Sep 17 00:00:00 2001 From: Auroter <7332587+Auroter@users.noreply.github.com> Date: Mon, 6 Feb 2023 17:54:39 -0700 Subject: [PATCH 29/35] fix member_invite deactivated at default (#108) --- ...platform_member-role_member-to-role_member-invite_apikey.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index b1fd169b..5e933b13 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -48,7 +48,7 @@ CREATE TABLE member_invite ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - expired_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + expired_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, accepted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, email TEXT NOT NULL, name TEXT DEFAULT '', From d31cdd24e08a48227cd4b2764063654d6e925e4f Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 7 Feb 2023 19:37:46 -0600 Subject: [PATCH 30/35] api description must default to null --- ...platform_member-role_member-to-role_member-invite_apikey.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql index 5e933b13..8cc24d81 100644 --- a/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql +++ b/migrations/0006_platform-member_member-to-platform_member-role_member-to-role_member-invite_apikey.sql @@ -66,7 +66,7 @@ CREATE TABLE apikey ( deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, type TEXT NOT NULL, -- [public,private] for now all public? data TEXT NOT NULL, -- the key itself - description TEXT NOT NULL, + description TEXT DEFAULT NULL, created_by UUID REFERENCES platform_member (id), platform_id UUID REFERENCES platform (id) ); From 2e5fe43a7c5ca7c20a3710bc07b0be5d35f3ff53 Mon Sep 17 00:00:00 2001 From: saito-sv <7920256+saito-sv@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:30:39 -0800 Subject: [PATCH 31/35] STR-429 (#109) * CI/CD * test deployment * updated configured creds version * revert changes for testing deployment --- .github/workflows/dev-deploy.yml | 58 ++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 7 ++-- 2 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/dev-deploy.yml diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml new file mode 100644 index 00000000..9665865f --- /dev/null +++ b/.github/workflows/dev-deploy.yml @@ -0,0 +1,58 @@ +name: deploy to development +permissions: + id-token: write + contents: read +on: + push: + branches: [develop] +jobs: + deploy: + environment: + name: development + url: https://string-api.dev.string-api.xyz + + name: build push and deploy + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: setup go + uses: actions/setup-go@v3 + with: + go-version-file: go.mod + cache: true + cache-dependency-path: go.sum + - name: install deps and build + ## TODO: Move all building into the docker container + run: | + go mod download + GOOS=linux GOARCH=amd64 go build -o ./cmd/app/main ./cmd/app/main.go + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v1.7.0 + with: + aws-region: us-west-2 + role-to-assume: ${{ secrets.ASSUME_ROLE }} + + - name: login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: tag and push to Amazon ECR + env: + ECR_REPO: ${{ secrets.AWS_ACCT }}.dkr.ecr.us-west-2.amazonaws.com + SERVICE: string-api + IMAGE_TAG: latest + run: | + docker build -t $ECR_REPO/$SERVICE:$IMAGE_TAG ./cmd/app/ + docker push $ECR_REPO/$SERVICE:$IMAGE_TAG + + - name: deploy + env: + CLUSTER: string-core + SERVICE: string-api + AWS_REGION: us-west-2 + run: | + aws ecs --region $AWS_REGION update-service --cluster $CLUSTER --service $SERVICE --force-new-deployment + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 122513f9..7c9b380f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,6 @@ on: pull_request: branches: - develop - name: run tests jobs: lint: @@ -17,7 +16,7 @@ jobs: cache: true cache-dependency-path: go.sum - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3 + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 - name: Run golangci-lint run: golangci-lint run --version --verbose --out-format=github-actions @@ -56,5 +55,5 @@ jobs: - name: Coveralls uses: coverallsapp/github-action@v1.1.2 with: - github-token: ${{ secrets.github_token }} - path-to-lcov: coverage.lcov \ No newline at end of file + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: coverage.lcov From 4102def43e90a0a58ae932f6d9c09c2ce735863a Mon Sep 17 00:00:00 2001 From: akfoster Date: Sun, 12 Feb 2023 13:34:16 -0600 Subject: [PATCH 32/35] Refactor Transaction Execute (#110) * first pass at refactoring executor * abstract card payment steps * more cleanup * s to p * cleaned up errors * fixing bugs * working as expected * remove sign_test changes * revert card changes * Added more comments to postProcess and moved it closer to the top of transaction.go * Update pkg/service/transaction.go * close executor earlier * clean up executor calls --------- Co-authored-by: Sean --- pkg/service/checkout.go | 2 +- pkg/service/transaction.go | 701 ++++++++++++++++++++----------------- 2 files changed, 373 insertions(+), 330 deletions(-) diff --git a/pkg/service/checkout.go b/pkg/service/checkout.go index 916a9bd2..62e1f847 100644 --- a/pkg/service/checkout.go +++ b/pkg/service/checkout.go @@ -74,7 +74,7 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au Type: checkoutCommon.Card, Number: "4242424242424242", // Success // Number: "4273149019799094", // succeed authorize, fail capture - // Number: "4544249167673670", // Declined - Insufficient funds + // Number: "4544249167673670", // Declined - Insufficient funds // Number: "5148447461737269", // Invalid transaction ExpiryMonth: 2, ExpiryYear: 2024, diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index f305a5b3..5341a402 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -50,6 +50,22 @@ type transaction struct { ids InternalIds } +type transactionProcessingData struct { + userId *string + deviceId *string + executor *Executor + processingFeeAsset *model.Asset + transactionModel *model.Transaction + chain *Chain + executionRequest *model.ExecutionRequest + cardAuthorization *AuthorizedCharge + preBalance *float64 + recipientWalletId *string + txId *string + cumulativeValue *big.Int + trueGas *uint64 +} + func NewTransaction(repos repository.Repositories, redis store.RedisStore) Transaction { return &transaction{repos: repos, redis: redis} } @@ -89,214 +105,264 @@ func (t transaction) Quote(d model.TransactionRequest) (model.ExecutionRequest, return res, nil } -func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId string) (model.TransactionReceipt, error) { - res := model.TransactionReceipt{} +func (t transaction) Execute(e model.ExecutionRequest, userId string, deviceId string) (res model.TransactionReceipt, err error) { t.getStringInstrumentsAndUserId() - user, err := t.repos.User.GetById(userId) + p := transactionProcessingData{executionRequest: &e, userId: &userId, deviceId: &deviceId} + + // Pre-flight transaction setup + p, err = t.transactionSetup(p) if err != nil { return res, common.StringError(err) } - if user.ID != userId { - return res, common.StringError(errors.New("not logged in")) - } - // Pull chain info needed for execution from repository - chain, err := ChainInfo(uint64(e.ChainID), t.repos.Network, t.repos.Asset) + // Run safety checks + p, err = t.safetyCheck(p) if err != nil { return res, common.StringError(err) } - // Create new Tx in repository, populate it with known info - db, err := t.repos.Transaction.Create(model.Transaction{Status: "Created", NetworkID: chain.UUID, DeviceID: deviceId, PlatformID: t.ids.StringPlatformId}) + // Send request to the blockchain and update model status, hash, transaction amount + p, err = t.initiateTransaction(p) if err != nil { return res, common.StringError(err) } - updateDB := &model.TransactionUpdates{} - processingFeeAsset, err := t.populateInitialTxModelData(e, updateDB) + // this Executor will not exist in scope of postProcess + (*p.executor).Close() + + // Send required information to new thread and return txId to the endpoint + go t.postProcess(p) + + return model.TransactionReceipt{TxID: *p.txId, TxURL: p.chain.Explorer + "/tx/" + *p.txId}, nil +} + +func (t transaction) postProcess(p transactionProcessingData) { + // Reinitialize Executor + executor := NewExecutor() + p.executor = &executor + err := executor.Initialize(p.chain.RPC) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to initialized executor in postProcess: %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - err = t.repos.Transaction.Update(db.ID, updateDB) + + // Update TX Status + updateDB := model.TransactionUpdates{} + status := "Post Process RPC Dialed" + updateDB.Status = &status + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) if err != nil { - fmt.Printf("\nERROR = %+v", err) - return res, common.StringError(err) + log.Printf("Failed to update transaction repo with status 'Post Process RPC Dialed': %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - // Dial the RPC and update model status - executor := NewExecutor() - err = executor.Initialize(chain.RPC) + // confirm the Tx on the EVM + trueGas, err := confirmTx(executor, *p.txId) + p.trueGas = &trueGas if err != nil { - return res, common.StringError(err) + log.Printf("Failed to confirm transaction: %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - status := "RPC Dialed" + + // Update DB status and NetworkFee + status = "Tx Confirmed" updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) + networkFee := strconv.FormatUint(trueGas, 10) + updateDB.NetworkFee = &networkFee // geth uses uint64 for gas + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to update transaction repo with status 'Tx Confirmed': %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - // Test the Tx and update model status - estimateUSD, estimateETH, err := t.testTransaction(executor, e.TransactionRequest, chain, false) + // Get new string wallet balance after executing the transaction + postBalance, err := executor.GetBalance() if err != nil { - return res, common.StringError(err) + log.Printf("Failed to get executor balance: %s", common.StringError(err)) + // TODO: handle error instead of returning it } - status = "Tested and Estimated" - updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) - if err != nil { - return res, common.StringError(err) + + // We can close the executor because we aren't using it after this + executor.Close() + + // If threshold was crossed, notify devs + // TODO: store threshold on a per-network basis in the repo + threshold := 10.0 + if *p.preBalance >= threshold && postBalance < threshold { + msg := fmt.Sprintf("STRING-API: %s balance is < %.2f at %.2f", p.chain.OwlracleName, threshold, postBalance) + err = MessageStaff(msg) + if err != nil { + log.Printf("Failed to send staff with low balance threshold message: %s", common.StringError(err)) + // Not seeing any e + // TODO: handle error instead of returning it + } } - // Verify the Quote and update model status - _, err = verifyQuote(e, estimateUSD) + // compute profit + // TODO: factor request.processingFeeAsset in the event of crypto-to-usd + profit, err := t.tenderTransaction(p) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to tender transaction: %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - status = "Quote Verified" + stringFee := floatToFixedString(profit, 6) + processingFee := floatToFixedString(profit, 6) // TODO: set processingFee based on payment method, and location + + // update db status and processing fees to db + updateDB.StringFee = &stringFee // string fee is always USD with 6 digits + updateDB.ProcessingFee = &processingFee + status = "Profit Tendered" updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to update transaction repo with status 'Profit Tendered': %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - // Get current balance of primary token - preBalance, err := executor.GetBalance() + // charge the users CC + err = t.chargeCard(p) if err != nil { - return res, common.StringError(err) - } - if preBalance < estimateETH { - msg := fmt.Sprintf("STRING-API: %s balance is too low to execute %.2f transaction at %.2f", chain.OwlracleName, estimateETH, preBalance) - MessageStaff(msg) - return res, common.StringError(errors.New("hot wallet ETH balance too low")) + log.Printf("Error, failed to charge card: %+v", common.StringError(err)) + // TODO: Handle error instead of returning it } - // Authorize quoted cost on end-user CC and update model status - cardAuthorization, err := t.authCard(e.UserAddress, e.CardToken, e.TotalUSD, processingFeeAsset, db.ID, userId) + // Update status upon success + status = "Card Charged" + updateDB.Status = &status + // TODO: Figure out how much we paid the CC payment processor and deduct it + // and use it to populate processing_fee and processing_fee_asset in the table + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to update transaction repo with status 'Card Charged': %s", common.StringError(err)) + // TODO: Handle error instead of returning it } - status = "Card " + cardAuthorization.Status + // Transaction complete! Update status + status = "Completed" updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) if err != nil { - return res, common.StringError(err) + log.Printf("Failed to update transaction repo with status 'Completed': %s", common.StringError(err)) } - recipientWalletId, err := t.addWalletInstrumentIdIfNew(e.UserAddress, user.ID) + // Create Transaction data in Unit21 + err = t.unit21CreateTransaction(p.transactionModel.ID) if err != nil { - return res, common.StringError(err) + log.Printf("Error creating Unit21 transaction: %s", common.StringError(err)) } - // TODO: Determine the output of the transaction (destination leg) with Tracers - destinationLeg := model.TxLeg{ - Timestamp: time.Now(), // Required by the db. Should be updated when the tx occurs - Amount: "0", // Required by Unit21. The amount of the asset received by the user - Value: "0", // Default to '0'. The value of the asset received by the user - AssetID: chain.GasTokenID, // Required by the db. the asset received by the user - UserID: user.ID, // the user who received the asset - InstrumentID: recipientWalletId, // Required by the db. the instrument which received the asset (wallet usually) + // send email receipt + err = t.sendEmailReceipt(p) + if err != nil { + log.Printf("Error sending email receipt to user: %s", common.StringError(err)) } +} - destinationLeg, err = t.repos.TxLeg.Create(destinationLeg) +func (t transaction) transactionSetup(p transactionProcessingData) (transactionProcessingData, error) { + // get user object + _, err := t.repos.User.GetById(*p.userId) if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - txLeg := model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID} - - err = t.repos.Transaction.Update(db.ID, txLeg) + // Pull chain info needed for execution from repository + chain, err := ChainInfo(uint64(p.executionRequest.ChainID), t.repos.Network, t.repos.Asset) + p.chain = &chain if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - if !cardAuthorization.Approved { - err := t.unit21CreateTransaction(db.ID) - if err != nil { - return res, common.StringError(err) - } + // Create new Tx in repository, populate it with known info + transactionModel, err := t.repos.Transaction.Create(model.Transaction{Status: "Created", NetworkID: chain.UUID, DeviceID: *p.deviceId, PlatformID: t.ids.StringPlatformId}) + p.transactionModel = &transactionModel + if err != nil { + return p, common.StringError(err) + } - return res, common.StringError(common.StringError(errors.New("payment: Authorization Declined by Checkout"))) + updateDB := &model.TransactionUpdates{} + processingFeeAsset, err := t.populateInitialTxModelData(*p.executionRequest, updateDB) + p.processingFeeAsset = &processingFeeAsset + if err != nil { + return p, common.StringError(err) + } + err = t.repos.Transaction.Update(transactionModel.ID, updateDB) + if err != nil { + fmt.Printf("\nERROR = %+v", common.StringError(err)) + return p, common.StringError(err) } - // Validate Transaction through Real Time Rules engine - u21auth, err := t.unit21Evaluate(db.ID) + // Dial the RPC and update model status + executor := NewExecutor() + p.executor = &executor + err = executor.Initialize(chain.RPC) if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - if !u21auth { - status = "Failed" - updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) - if err != nil { - return res, common.StringError(err) - } + err = t.updateTransactionStatus("RPC Dialed", transactionModel.ID) + if err != nil { + return p, common.StringError(err) + } - err = t.unit21CreateTransaction(db.ID) - if err != nil { - return res, common.StringError(err) - } + return p, err +} - return res, common.StringError(errors.New("risk: Transaction Failed Unit21 Real Time Rules Evaluation")) +func (t transaction) safetyCheck(p transactionProcessingData) (transactionProcessingData, error) { + // Test the Tx and update model status + estimateUSD, estimateETH, err := t.testTransaction(*p.executor, p.executionRequest.TransactionRequest, *p.chain, false) + if err != nil { + return p, common.StringError(err) } - status = "Unit21 Authorized" - updateDB.Status = &status - err = t.repos.Transaction.Update(db.ID, updateDB) + err = t.updateTransactionStatus("Tested and Estimated", p.transactionModel.ID) if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - // Send request to the blockchain and update model status, hash, transaction amount - txID, value, err := t.initiateTransaction(executor, e, processingFeeAsset, db.ID, userId) + // Verify the Quote and update model status + _, err = verifyQuote(*p.executionRequest, estimateUSD) if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - status = "Transaction Initiated" - updateDB.Status = &status - updateDB.TransactionHash = &txID - txAmount := value.String() - updateDB.TransactionAmount = &txAmount - err = t.repos.Transaction.Update(db.ID, updateDB) + err = t.updateTransactionStatus("Quote Verified", p.transactionModel.ID) if err != nil { - return res, common.StringError(err) + return p, common.StringError(err) } - // this Executor will not exist in scope of postProcess - executor.Close() + // Get current balance of primary token + preBalance, err := (*p.executor).GetBalance() + p.preBalance = &preBalance + if err != nil { + return p, common.StringError(err) + } + if preBalance < estimateETH { + msg := fmt.Sprintf("STRING-API: %s balance is too low to execute %.2f transaction at %.2f", p.chain.OwlracleName, estimateETH, preBalance) + MessageStaff(msg) + return p, common.StringError(errors.New("hot wallet ETH balance too low")) + } - // Send required information to new thread and return TxID to the endpoint - post := postProcessRequest{ - TxID: txID, - Chain: chain, - Authorization: cardAuthorization, - UserAddress: e.UserAddress, - CumulativeValue: value, - Quote: e.Quote, - TxDBID: db.ID, - processingFeeAsset: processingFeeAsset, - preBalance: preBalance, - userId: userId, - recipientWalletId: recipientWalletId, - } - go t.postProcess(post) - - return model.TransactionReceipt{TxID: txID, TxURL: chain.Explorer + "/tx/" + txID}, nil -} + // Authorize quoted cost on end-user CC and update model status + p, err = t.authCard(p) + if err != nil { + return p, common.StringError(err) + } -func (t *transaction) getStringInstrumentsAndUserId() { - t.ids = GetStringIdsFromEnv() + // Validate Transaction through Real Time Rules engine + err = t.unit21Evaluate(p.transactionModel.ID) + if err != nil { + return p, common.StringError(err) + } + + return p, nil } func (t transaction) populateInitialTxModelData(e model.ExecutionRequest, m *model.TransactionUpdates) (model.Asset, error) { txType := "fiat-to-crypto" m.Type = &txType - // TODO populate db.Tags with key-val pairs for Unit21 - // TODO populate db.DeviceID with info from fingerprint - // TODO populate db.IPAddress with info from fingerprint - // TODO populate db.PlatformID with UUID of customer + // TODO populate transactionModel.Tags with key-val pairs for Unit21 + // TODO populate transactionModel.DeviceID with info from fingerprint + // TODO populate transactionModel.IPAddress with info from fingerprint + // TODO populate transactionModel.PlatformID with UUID of customer // bytes, err := json.Marshal() contractParams := pq.StringArray(e.CxParams) @@ -424,79 +490,136 @@ func (t transaction) addWalletInstrumentIdIfNew(address string, id string) (stri return instrument.ID, nil } -func (t transaction) authCard(userWallet string, cardToken string, usd float64, chargeAsset model.Asset, dbID string, userId string) (AuthorizedCharge, error) { +func (t transaction) authCard(p transactionProcessingData) (transactionProcessingData, error) { // auth their card - auth, err := AuthorizeCharge(usd, userWallet, cardToken) + auth, err := AuthorizeCharge(p.executionRequest.TotalUSD, p.executionRequest.UserAddress, p.executionRequest.CardToken) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } // Add Checkout Instrument ID to our DB if it's not there already and associate it with the user - instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, userId, auth.Last4, auth.CardType) + instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, *p.userId, auth.Last4, auth.CardType) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } // Create Origin Tx leg - usdWei := floatToFixedString(usd, int(chargeAsset.Decimals)) + usdWei := floatToFixedString(p.executionRequest.TotalUSD, int(p.processingFeeAsset.Decimals)) origin := model.TxLeg{ Timestamp: time.Now(), Amount: usdWei, Value: usdWei, - AssetID: chargeAsset.ID, - UserID: userId, + AssetID: p.processingFeeAsset.ID, + UserID: *p.userId, InstrumentID: instrumentId, } origin, err = t.repos.TxLeg.Create(origin) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) + } + txLegUpdates := model.TransactionUpdates{OriginTxLegID: &origin.ID} + err = t.repos.Transaction.Update(p.transactionModel.ID, txLegUpdates) + if err != nil { + return p, common.StringError(err) + } + + p.cardAuthorization = &auth + if err != nil { + return p, common.StringError(err) + } + err = t.updateTransactionStatus("Card "+auth.Status, p.transactionModel.ID) + if err != nil { + return p, common.StringError(err) } - txLeg := model.TransactionUpdates{OriginTxLegID: &origin.ID} - err = t.repos.Transaction.Update(dbID, txLeg) + + recipientWalletId, err := t.addWalletInstrumentIdIfNew(p.executionRequest.UserAddress, *p.userId) + p.recipientWalletId = &recipientWalletId + if err != nil { + return p, common.StringError(err) + } + + // TODO: Determine the output of the transaction (destination leg) with Tracers + destinationLeg := model.TxLeg{ + Timestamp: time.Now(), // Required by the db. Should be updated when the tx occurs + Amount: "0", // Required by Unit21. The amount of the asset received by the user + Value: "0", // Default to '0'. The value of the asset received by the user + AssetID: p.chain.GasTokenID, // Required by the db. the asset received by the user + UserID: *p.userId, // the user who received the asset + InstrumentID: recipientWalletId, // Required by the db. the instrument which received the asset (wallet usually) + } + + destinationLeg, err = t.repos.TxLeg.Create(destinationLeg) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } - return auth, nil + txLegUpdates = model.TransactionUpdates{DestinationTxLegID: &destinationLeg.ID} + + err = t.repos.Transaction.Update(p.transactionModel.ID, txLegUpdates) + if err != nil { + return p, common.StringError(err) + } + + if !auth.Approved { + err := t.unit21CreateTransaction(p.transactionModel.ID) + if err != nil { + return p, common.StringError(err) + } + + return p, common.StringError(errors.New("payment: Authorization Declined by Checkout")) + } + + return p, nil } -func (t transaction) initiateTransaction(executor Executor, e model.ExecutionRequest, chargeAsset model.Asset, txUUID string, userId string) (string, *big.Int, error) { +func (t transaction) initiateTransaction(p transactionProcessingData) (transactionProcessingData, error) { call := ContractCall{ - CxAddr: e.CxAddr, - CxFunc: e.CxFunc, - CxReturn: e.CxReturn, - CxParams: e.CxParams, - TxValue: e.TxValue, - TxGasLimit: e.TxGasLimit, + CxAddr: p.executionRequest.CxAddr, + CxFunc: p.executionRequest.CxFunc, + CxReturn: p.executionRequest.CxReturn, + CxParams: p.executionRequest.CxParams, + TxValue: p.executionRequest.TxValue, + TxGasLimit: p.executionRequest.TxGasLimit, } - txID, value, err := executor.Initiate(call) + + txID, value, err := (*p.executor).Initiate(call) + p.cumulativeValue = value if err != nil { - return "", nil, common.StringError(err) + return p, common.StringError(err) } + p.txId = &txID // Create Response Tx leg eth := common.WeiToEther(value) wei := floatToFixedString(eth, 18) - usd := floatToFixedString(e.TotalUSD, int(chargeAsset.Decimals)) + usd := floatToFixedString(p.executionRequest.TotalUSD, int(p.processingFeeAsset.Decimals)) responseLeg := model.TxLeg{ Timestamp: time.Now(), Amount: wei, Value: usd, - AssetID: chargeAsset.ID, - UserID: userId, + AssetID: p.processingFeeAsset.ID, + UserID: *p.userId, InstrumentID: t.ids.StringWalletId, } responseLeg, err = t.repos.TxLeg.Create(responseLeg) if err != nil { - return txID, value, common.StringError(err) + return p, common.StringError(err) } txLeg := model.TransactionUpdates{ResponseTxLegID: &responseLeg.ID} - err = t.repos.Transaction.Update(txUUID, txLeg) + err = t.repos.Transaction.Update(p.transactionModel.ID, txLeg) if err != nil { - return txID, value, common.StringError(err) + return p, common.StringError(err) } - return txID, value, nil + status := "Transaction Initiated" + txAmount := p.cumulativeValue.String() + updateDB := &model.TransactionUpdates{Status: &status, TransactionHash: p.txId, TransactionAmount: &txAmount} + err = t.repos.Transaction.Update(p.transactionModel.ID, updateDB) + if err != nil { + return p, common.StringError(err) + } + + return p, nil } func confirmTx(executor Executor, txID string) (uint64, error) { @@ -507,45 +630,16 @@ func confirmTx(executor Executor, txID string) (uint64, error) { return trueGas, nil } -func (t transaction) chargeCard(userWallet string, authorizationID string, usd float64, chargeAsset model.Asset, txUUID string, userId string) error { - _, err := CaptureCharge(usd, userWallet, authorizationID) - if err != nil { - return common.StringError(err) - } - - // Create Receipt Tx leg - usdWei := floatToFixedString(usd, int(chargeAsset.Decimals)) - receiptLeg := model.TxLeg{ - Timestamp: time.Now(), - Amount: usdWei, - Value: usdWei, - AssetID: chargeAsset.ID, - UserID: t.ids.StringUserId, - InstrumentID: t.ids.StringBankId, - } - receiptLeg, err = t.repos.TxLeg.Create(receiptLeg) - if err != nil { - return common.StringError(err) - } - txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID} - err = t.repos.Transaction.Update(txUUID, txLeg) - if err != nil { - return common.StringError(err) - } - - return nil -} - // TODO: rewrite this transaction to reference the asset(s) received by the user, not what we paid -func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas uint64, quotedTotal float64, chain Chain, txUUID string, recipientId string, userWalletId string) (float64, error) { +func (t transaction) tenderTransaction(p transactionProcessingData) (float64, error) { cost := NewCost(t.redis) - trueWei := big.NewInt(0).Add(cumulativeValue, big.NewInt(int64(cumulativeGas))) + trueWei := big.NewInt(0).Add(p.cumulativeValue, big.NewInt(int64(*p.trueGas))) trueEth := common.WeiToEther(trueWei) - trueUSD, err := cost.LookupUSD(chain.CoingeckoName, trueEth) + trueUSD, err := cost.LookupUSD(p.chain.CoingeckoName, trueEth) if err != nil { return 0, common.StringError(err) } - profit := quotedTotal - trueUSD + profit := p.executionRequest.Quote.TotalUSD - trueUSD // Create Receive Tx leg asset, err := t.repos.Asset.GetName("ETH") @@ -553,21 +647,21 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u return profit, common.StringError(err) } wei := floatToFixedString(trueEth, int(asset.Decimals)) - usd := floatToFixedString(quotedTotal, 6) + usd := floatToFixedString(p.executionRequest.Quote.TotalUSD, 6) - txModel, err := t.repos.Transaction.GetById(txUUID) + txModel, err := t.repos.Transaction.GetById(p.transactionModel.ID) if err != nil { return profit, common.StringError(err) } now := time.Now() destinationLeg := model.TxLegUpdates{ - Timestamp: &now, // updated based on *when the transaction occured* not time.Now() - Amount: &wei, // Should be the amount of the asset received by the user - Value: &usd, // The value of the asset received by the user - AssetID: &asset.ID, // the asset received by the user - UserID: &recipientId, // the user who received the asset - InstrumentID: &userWalletId, // the instrument which received the asset (wallet usually) + Timestamp: &now, // updated based on *when the transaction occured* not time.Now() + Amount: &wei, // Should be the amount of the asset received by the user + Value: &usd, // The value of the asset received by the user + AssetID: &asset.ID, // the asset received by the user + UserID: p.userId, // the user who received the asset + InstrumentID: p.recipientWalletId, // the instrument which received the asset (wallet usually) } // We now update the destination leg instead of creating it @@ -579,127 +673,45 @@ func (t transaction) tenderTransaction(cumulativeValue *big.Int, cumulativeGas u return profit, nil } -type postProcessRequest struct { - TxID string - Chain Chain - Authorization AuthorizedCharge - UserAddress string - CumulativeGas uint64 - CumulativeValue *big.Int - Quote model.Quote - TxDBID string - processingFeeAsset model.Asset - preBalance float64 - userId string - recipientWalletId string -} - -func (t transaction) postProcess(request postProcessRequest) { - executor := NewExecutor() - err := executor.Initialize(request.Chain.RPC) - if err != nil { - // TODO: Handle error instead of returning it - } - updateDB := model.TransactionUpdates{} - status := "Post Process RPC Dialed" - updateDB.Status = &status - err = t.repos.Transaction.Update(request.TxDBID, updateDB) - if err != nil { - // TODO: Handle error instead of returning it - } - - // confirm the Tx on the EVM, update db status and NetworkFee - trueGas, err := confirmTx(executor, request.TxID) - if err != nil { - // TODO: Handle error instead of returning it - } - status = "Tx Confirmed" - updateDB.Status = &status - networkFee := strconv.FormatUint(trueGas, 10) - updateDB.NetworkFee = &networkFee // geth uses uint64 for gas - err = t.repos.Transaction.Update(request.TxDBID, updateDB) - if err != nil { - // TODO: Handle error instead of returning it - } - - // Check and see if balance threshold was crossed - postBalance, err := executor.GetBalance() - if err != nil { - // TODO: handle error instead of returning it - } - // TODO: store threshold on a per-network basis in the repo - threshold := 10.0 - if request.preBalance >= threshold && postBalance < threshold { - msg := fmt.Sprintf("STRING-API: %s balance is < %.2f at %.2f", request.Chain.OwlracleName, threshold, postBalance) - MessageStaff(msg) - if err != nil { - // TODO: handle error instead of returning it - } - } - - // compute profit, update db status and processing fees to db - // TODO: factor request.processingFeeAsset in the event of crypto-to-usd - profit, err := t.tenderTransaction(request.CumulativeValue, trueGas, request.Quote.TotalUSD, request.Chain, request.TxDBID, request.userId, request.recipientWalletId) - if err != nil { - // TODO: Handle error instead of returning it - } - fmt.Printf("PROFIT=%+v", profit) - status = "Profit Tendered" - updateDB.Status = &status - stringFee := floatToFixedString(profit, 6) - processingFee := floatToFixedString(profit, 6) // TODO: set processingFee based on payment method, and location - updateDB.StringFee = &stringFee // string fee is always USD with 6 digits - updateDB.ProcessingFee = &processingFee - err = t.repos.Transaction.Update(request.TxDBID, updateDB) +func (t transaction) chargeCard(p transactionProcessingData) error { + _, err := CaptureCharge(p.executionRequest.Quote.TotalUSD, p.executionRequest.UserAddress, p.cardAuthorization.AuthID) if err != nil { - // TODO: Handle error instead of returning it + return common.StringError(err) } - // charge the users CC - err = t.chargeCard(request.UserAddress, request.Authorization.AuthID, request.Quote.TotalUSD, request.processingFeeAsset, request.TxDBID, request.userId) - if err != nil { - // TODO: Handle error instead of returning it - } - status = "Card Charged" - updateDB.Status = &status - // TODO: Figure out how much we paid the CC payment processor and deduct it - // and use it to populate processing_fee and processing_fee_asset in the table - err = t.repos.Transaction.Update(request.TxDBID, updateDB) - if err != nil { - // TODO: Handle error instead of returning it + // Create Receipt Tx leg + usdWei := floatToFixedString(p.executionRequest.Quote.TotalUSD, int(p.processingFeeAsset.Decimals)) + receiptLeg := model.TxLeg{ + Timestamp: time.Now(), + Amount: usdWei, + Value: usdWei, + AssetID: p.processingFeeAsset.ID, + UserID: t.ids.StringUserId, + InstrumentID: t.ids.StringBankId, } - - status = "Completed" - updateDB.Status = &status - err = t.repos.Transaction.Update(request.TxDBID, updateDB) + receiptLeg, err = t.repos.TxLeg.Create(receiptLeg) if err != nil { - // TODO: Handle error instead of returning it + return common.StringError(err) } - executor.Close() - // Create Transaction data in Unit21 - - err = t.unit21CreateTransaction(request.TxDBID) + txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID} + err = t.repos.Transaction.Update(p.transactionModel.ID, txLeg) if err != nil { - log.Printf("Error creating Unit21 transaction: %s", err) + return common.StringError(err) } - // send email receipt - err = t.sendEmailReceipt(request) - if err != nil { - log.Printf("Error sending email receipt to user: %s", err) - } + return nil } -func (t transaction) sendEmailReceipt(request postProcessRequest) error { - user, err := t.repos.User.GetById(request.userId) +func (t transaction) sendEmailReceipt(p transactionProcessingData) error { + user, err := t.repos.User.GetById(*p.userId) if err != nil { - log.Printf("Error getting user from repo: %s", err) - return err + log.Printf("Error getting user from repo: %s", common.StringError(err)) + return common.StringError(err) } - contact, err := t.repos.Contact.GetByUserId(request.userId) + contact, err := t.repos.Contact.GetByUserId(user.ID) if err != nil { - log.Printf("Error getting user contact from repo: %s", err) - return err + log.Printf("Error getting user contact from repo: %s", common.StringError(err)) + return common.StringError(err) } name := user.FirstName // + " " + user.MiddleName + " " + user.LastName if name == "" { @@ -708,27 +720,27 @@ func (t transaction) sendEmailReceipt(request postProcessRequest) error { receiptParams := common.ReceiptGenerationParams{ ReceiptType: "NFT Purchase", // TODO: retrieve dynamically CustomerName: name, - StringPaymentId: request.TxDBID, + StringPaymentId: p.transactionModel.ID, PaymentDescriptor: "String Digital Asset", // TODO: retrieve dynamically TransactionDate: time.Now().Format(time.RFC1123), } receiptBody := [][2]string{ - {"Transaction ID", "" + request.TxID + ""}, - {"Destination Wallet", "" + request.UserAddress + ""}, + {"Transaction ID", "" + *p.txId + ""}, + {"Destination Wallet", "" + p.executionRequest.UserAddress + ""}, {"Payment Descriptor", receiptParams.PaymentDescriptor}, - {"Payment Method", request.Authorization.Issuer + " " + request.Authorization.Last4}, + {"Payment Method", p.cardAuthorization.Issuer + " " + p.cardAuthorization.Last4}, {"Platform", "String Demo"}, // TODO: retrieve dynamically {"Item Ordered", "String Fighter NFT"}, // TODO: retrieve dynamically {"Token ID", "1234"}, // TODO: retrieve dynamically, maybe after building token transfer detection - {"Subtotal", common.FloatToUSDString(request.Quote.BaseUSD + request.Quote.TokenUSD)}, - {"Network Fee:", common.FloatToUSDString(request.Quote.GasUSD)}, - {"Processing Fee", common.FloatToUSDString(request.Quote.ServiceUSD)}, - {"Total Charge", common.FloatToUSDString(request.Quote.TotalUSD)}, + {"Subtotal", common.FloatToUSDString(p.executionRequest.Quote.BaseUSD + p.executionRequest.Quote.TokenUSD)}, + {"Network Fee:", common.FloatToUSDString(p.executionRequest.Quote.GasUSD)}, + {"Processing Fee", common.FloatToUSDString(p.executionRequest.Quote.ServiceUSD)}, + {"Total Charge", common.FloatToUSDString(p.executionRequest.Quote.TotalUSD)}, } err = common.EmailReceipt(contact.Data, receiptParams, receiptBody) if err != nil { - log.Printf("Error sending email receipt to user: %s", err) - return err + log.Printf("Error sending email receipt to user: %s", common.StringError(err)) + return common.StringError(err) } return nil } @@ -748,7 +760,7 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er u21InstrumentId, err := u21Instrument.Create(instrument) if err != nil { fmt.Printf("Error creating new instrument in Unit21") - return + return common.StringError(err) } // Log create instrument action w/ Unit21 @@ -762,7 +774,7 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er _, err = u21Action.Create(instrument, "Creation", u21InstrumentId, "Creation") if err != nil { fmt.Printf("Error creating a new instrument action in Unit21") - return + return common.StringError(err) } return @@ -771,8 +783,8 @@ func (t transaction) unit21CreateInstrument(instrument model.Instrument) (err er func (t transaction) unit21CreateTransaction(transactionId string) (err error) { txModel, err := t.repos.Transaction.GetById(transactionId) if err != nil { - log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", err) - return + log.Printf("Error getting tx model in Unit21 in Tx Postprocess: %s", common.StringError(err)) + return common.StringError(err) } u21Repo := unit21.TransactionRepo{ @@ -784,19 +796,19 @@ func (t transaction) unit21CreateTransaction(transactionId string) (err error) { u21Tx := unit21.NewTransaction(u21Repo) _, err = u21Tx.Create(txModel) if err != nil { - log.Printf("Error updating Unit21 in Tx Postprocess: %s", err) - return + log.Printf("Error updating Unit21 in Tx Postprocess: %s", common.StringError(err)) + return common.StringError(err) } - return + return nil } -func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err error) { +func (t transaction) unit21Evaluate(transactionId string) (err error) { //Check transaction in Unit21 txModel, err := t.repos.Transaction.GetById(transactionId) if err != nil { - log.Printf("Error getting tx model in Unit21 in Tx Evaluate: %s", err) - return evaluation, common.StringError(err) + log.Printf("Error getting tx model in Unit21 in Tx Evaluate: %s", common.StringError(err)) + return common.StringError(err) } u21Repo := unit21.TransactionRepo{ @@ -806,12 +818,43 @@ func (t transaction) unit21Evaluate(transactionId string) (evaluation bool, err } u21Tx := unit21.NewTransaction(u21Repo) - evaluation, err = u21Tx.Evaluate(txModel) + evaluation, err := u21Tx.Evaluate(txModel) if err != nil { - log.Printf("Error evaluating transaction in Unit21: %s", err) - return evaluation, common.StringError(err) + log.Printf("Error evaluating transaction in Unit21: %s", common.StringError(err)) + return common.StringError(err) } - return + if !evaluation { + err = t.updateTransactionStatus("Failed", transactionId) + if err != nil { + return common.StringError(err) + } + err = t.unit21CreateTransaction(transactionId) + if err != nil { + return common.StringError(err) + } + + return common.StringError(errors.New("risk: Transaction Failed Unit21 Real Time Rules Evaluation")) + } + err = t.updateTransactionStatus("Unit21 Authorized", transactionId) + if err != nil { + return common.StringError(err) + } + + return nil +} + +func (t transaction) updateTransactionStatus(status string, transactionId string) (err error) { + updateDB := &model.TransactionUpdates{Status: &status} + err = t.repos.Transaction.Update(transactionId, updateDB) + if err != nil { + return common.StringError(err) + } + + return nil +} + +func (t *transaction) getStringInstrumentsAndUserId() { + t.ids = GetStringIdsFromEnv() } From 12b1efbfbcf664e4a00a20b4df6224224b2c811e Mon Sep 17 00:00:00 2001 From: akfoster Date: Mon, 13 Feb 2023 15:23:59 -0600 Subject: [PATCH 33/35] Task/andrin/str 354 (#115) * only a name change needed * cleanup of card creation and payment * set authorizeCharge to take transasctionProcessingData struct * move default cc back to success * simplify GetCardByFingerprint wrapper of GetWalletByAddr --- pkg/model/request.go | 12 ++++++++++++ pkg/repository/instrument.go | 13 +++++++++---- pkg/service/auth.go | 4 ++-- pkg/service/checkout.go | 25 +++++++++++++------------ pkg/service/transaction.go | 32 +++++++++++++++++--------------- pkg/service/user.go | 2 +- 6 files changed, 54 insertions(+), 34 deletions(-) diff --git a/pkg/model/request.go b/pkg/model/request.go index 9f3ff64a..ca56c48e 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -1,6 +1,7 @@ package model import ( + "database/sql" "time" "github.com/jmoiron/sqlx/types" @@ -32,6 +33,17 @@ type TransactionUpdates struct { StringFee *string `json:"stringFee" db:"string_fee"` } +type InstrumentUpdates struct { + Type *string `json:"type" db:"type"` + Status *string `json:"status" db:"status"` + Tags *StringMap `json:"tags" db:"tags"` + Network *string `json:"network" db:"network"` + PublicKey *string `json:"publicKey" db:"public_key"` + Last4 *string `json:"last4" db:"last_4"` + UserID *string `json:"userId" db:"user_id"` + LocationID *sql.NullString `json:"locationId" db:"location_id"` +} + type TxLegUpdates struct { Timestamp *time.Time `json:"timestamp" db:"timestamp"` Amount *string `json:"amount" db:"amount"` diff --git a/pkg/repository/instrument.go b/pkg/repository/instrument.go index 6a816bc6..c6e17c30 100644 --- a/pkg/repository/instrument.go +++ b/pkg/repository/instrument.go @@ -13,9 +13,10 @@ import ( type Instrument interface { Transactable Create(model.Instrument) (model.Instrument, error) - GetById(id string) (model.Instrument, error) - GetWallet(addr string) (model.Instrument, error) Update(ID string, updates any) error + GetById(id string) (model.Instrument, error) + GetWalletByAddr(addr string) (model.Instrument, error) + GetCardByFingerprint(fingerprint string) (m model.Instrument, err error) GetWalletByUserId(userId string) (model.Instrument, error) GetBankByUserId(userId string) (model.Instrument, error) WalletAlreadyExists(addr string) (bool, error) @@ -48,7 +49,7 @@ func (i instrument[T]) Create(insert model.Instrument) (model.Instrument, error) return m, nil } -func (i instrument[T]) GetWallet(addr string) (model.Instrument, error) { +func (i instrument[T]) GetWalletByAddr(addr string) (model.Instrument, error) { m := model.Instrument{} err := i.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE public_key = $1", i.table), addr) if err != nil && err == sql.ErrNoRows { @@ -59,6 +60,10 @@ func (i instrument[T]) GetWallet(addr string) (model.Instrument, error) { return m, nil } +func (i instrument[T]) GetCardByFingerprint(fingerprint string) (m model.Instrument, err error) { + return i.GetWalletByAddr(fingerprint) +} + func (i instrument[T]) GetWalletByUserId(userId string) (model.Instrument, error) { m := model.Instrument{} err := i.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE user_id = $1 AND type = 'Crypto Wallet'", i.table), userId) @@ -82,7 +87,7 @@ func (i instrument[T]) GetBankByUserId(userId string) (model.Instrument, error) } func (i instrument[T]) WalletAlreadyExists(addr string) (bool, error) { - wallet, err := i.GetWallet(addr) + wallet, err := i.GetWalletByAddr(addr) if err != nil && errors.Cause(err).Error() != "not found" { // because we are wrapping error and care about its value return true, common.StringError(err) diff --git a/pkg/service/auth.go b/pkg/service/auth.go index e500d038..1eed666a 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -97,7 +97,7 @@ func (a auth) VerifySignedPayload(request model.WalletSignaturePayloadSigned) (U } // Verify user is registered to this wallet address - instrument, err := a.repos.Instrument.GetWallet(payload.Address) + instrument, err := a.repos.Instrument.GetWalletByAddr(payload.Address) if err != nil { return resp, common.StringError(err) } @@ -204,7 +204,7 @@ func (a auth) RefreshToken(refreshToken string, walletAddress string) (UserCreat // verify wallet address // Verify user is registered to this wallet address - instrument, err := a.repos.Instrument.GetWallet(walletAddress) + instrument, err := a.repos.Instrument.GetWalletByAddr(walletAddress) if err != nil { if strings.Contains(err.Error(), "not found") { return resp, common.StringError(errors.New("wallet address not associated with this user: " + walletAddress)) diff --git a/pkg/service/checkout.go b/pkg/service/checkout.go index 62e1f847..62d8910f 100644 --- a/pkg/service/checkout.go +++ b/pkg/service/checkout.go @@ -59,10 +59,11 @@ type AuthorizedCharge struct { CardType string } -func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth AuthorizedCharge, err error) { +func AuthorizeCharge(p transactionProcessingData) (transactionProcessingData, error) { + auth := AuthorizedCharge{} config, err := getConfig() if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } client := payments.NewClient(*config) @@ -75,7 +76,7 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au Number: "4242424242424242", // Success // Number: "4273149019799094", // succeed authorize, fail capture // Number: "4544249167673670", // Declined - Insufficient funds - // Number: "5148447461737269", // Invalid transaction + // Number: "5148447461737269", // Invalid transaction (debit card) ExpiryMonth: 2, ExpiryYear: 2024, Name: "Customer Name", @@ -83,17 +84,17 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au } paymentToken, err := CreateToken(&card) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } paymentTokenID = paymentToken.Created.Token - if tokenId != "" { - paymentTokenID = tokenId + if p.executionRequest.CardToken != "" { + paymentTokenID = p.executionRequest.CardToken } } else { - paymentTokenID = tokenId + paymentTokenID = p.executionRequest.CardToken } - usd := convertAmount(amount) + usd := convertAmount(p.executionRequest.TotalUSD) capture := false request := &payments.Request{ @@ -104,7 +105,7 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au Amount: usd, Currency: "USD", Customer: &payments.Customer{ - Name: userWallet, + Name: p.executionRequest.UserAddress, }, Capture: &capture, } @@ -115,7 +116,7 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au } response, err := client.Request(request, ¶ms) if err != nil { - return auth, common.StringError(err) + return p, common.StringError(err) } // Collect authorization ID and Instrument ID @@ -132,9 +133,9 @@ func AuthorizeCharge(amount float64, userWallet string, tokenId string) (auth Au auth.CheckoutFingerprint = response.Processed.Source.CardSourceResponse.Fingerprint } } - + p.cardAuthorization = &auth // TODO: Create entry for authorization in our DB associated with userWallet - return auth, nil + return p, nil } func CaptureCharge(amount float64, userWallet string, authorizationID string) (capture *payments.CapturesResponse, err error) { diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 5341a402..936fe7fa 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -449,8 +449,8 @@ func verifyQuote(e model.ExecutionRequest, newEstimate model.Quote) (bool, error return true, nil } -func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, last4 string, cardType string) (string, error) { - instrument, err := t.repos.Instrument.GetWallet(fingerprint) // temporarily using get wallet and storing it there +func (t transaction) addCardInstrumentIdIfNew(p transactionProcessingData) (string, error) { + instrument, err := t.repos.Instrument.GetCardByFingerprint(p.cardAuthorization.CheckoutFingerprint) if err != nil && !strings.Contains(err.Error(), "not found") { // because we are wrapping error and care about its value return "", common.StringError(err) } else if err == nil && instrument.UserID != "" { @@ -458,12 +458,18 @@ func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, } // We should gather type from the payment processor - instrument_type := "DebitCard" - if cardType == "CREDIT" { - instrument_type = "CreditCard" + instrument_type := "Debit Card" + if p.cardAuthorization.CardType == "CREDIT" { + instrument_type = "Credit Card" } // Create a new instrument - instrument = model.Instrument{Type: instrument_type, Status: "authorized", Last4: last4, UserID: userID, PublicKey: fingerprint} // No locationID until fingerprint + instrument = model.Instrument{ // No locationID until fingerprint + Type: instrument_type, + Status: "created", + Last4: p.cardAuthorization.Last4, + UserID: *p.userId, + PublicKey: p.cardAuthorization.CheckoutFingerprint, + } instrument, err = t.repos.Instrument.Create(instrument) if err != nil { return "", common.StringError(err) @@ -473,7 +479,7 @@ func (t transaction) addCardInstrumentIdIfNew(fingerprint string, userID string, } func (t transaction) addWalletInstrumentIdIfNew(address string, id string) (string, error) { - instrument, err := t.repos.Instrument.GetWallet(address) + instrument, err := t.repos.Instrument.GetWalletByAddr(address) if err != nil && !strings.Contains(err.Error(), "not found") { return "", common.StringError(err) } else if err == nil && instrument.PublicKey == address { @@ -492,13 +498,13 @@ func (t transaction) addWalletInstrumentIdIfNew(address string, id string) (stri func (t transaction) authCard(p transactionProcessingData) (transactionProcessingData, error) { // auth their card - auth, err := AuthorizeCharge(p.executionRequest.TotalUSD, p.executionRequest.UserAddress, p.executionRequest.CardToken) + p, err := AuthorizeCharge(p) if err != nil { return p, common.StringError(err) } // Add Checkout Instrument ID to our DB if it's not there already and associate it with the user - instrumentId, err := t.addCardInstrumentIdIfNew(auth.CheckoutFingerprint, *p.userId, auth.Last4, auth.CardType) + instrumentId, err := t.addCardInstrumentIdIfNew(p) if err != nil { return p, common.StringError(err) } @@ -523,11 +529,7 @@ func (t transaction) authCard(p transactionProcessingData) (transactionProcessin return p, common.StringError(err) } - p.cardAuthorization = &auth - if err != nil { - return p, common.StringError(err) - } - err = t.updateTransactionStatus("Card "+auth.Status, p.transactionModel.ID) + err = t.updateTransactionStatus("Card "+p.cardAuthorization.Status, p.transactionModel.ID) if err != nil { return p, common.StringError(err) } @@ -560,7 +562,7 @@ func (t transaction) authCard(p transactionProcessingData) (transactionProcessin return p, common.StringError(err) } - if !auth.Approved { + if !p.cardAuthorization.Approved { err := t.unit21CreateTransaction(p.transactionModel.ID) if err != nil { return p, common.StringError(err) diff --git a/pkg/service/user.go b/pkg/service/user.go index 1d205ac1..303c213e 100644 --- a/pkg/service/user.go +++ b/pkg/service/user.go @@ -150,7 +150,7 @@ func (u user) createUserData(addr string) (model.User, error) { return user, common.StringError(err) } // Create a new wallet instrument and associate it with the new user - instrument := model.Instrument{Type: "crypto-wallet", Status: "verified", Network: "EVM", PublicKey: addr, UserID: user.ID} + instrument := model.Instrument{Type: "Crypto Wallet", Status: "verified", Network: "EVM", PublicKey: addr, UserID: user.ID} instrument, err = u.repos.Instrument.Create(instrument) if err != nil { u.repos.Instrument.Rollback() From 3c7990bae900c67cbbda65bf9e30ef72388c39c9 Mon Sep 17 00:00:00 2001 From: akfoster Date: Mon, 13 Feb 2023 16:55:24 -0600 Subject: [PATCH 34/35] rename to match style guide; get asset based on chain (#117) --- pkg/repository/asset.go | 4 ++-- pkg/repository/network.go | 4 ++-- pkg/service/chain.go | 2 +- pkg/service/executor.go | 4 ++-- pkg/service/transaction.go | 6 +++--- pkg/test/stubs/repository.go | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/repository/asset.go b/pkg/repository/asset.go index e34e10ef..d0f984e1 100644 --- a/pkg/repository/asset.go +++ b/pkg/repository/asset.go @@ -13,7 +13,7 @@ type Asset interface { Transactable Create(model.Asset) (model.Asset, error) GetById(id string) (model.Asset, error) - GetName(name string) (model.Asset, error) + GetByName(name string) (model.Asset, error) Update(ID string, updates any) error } @@ -41,7 +41,7 @@ func (a asset[T]) Create(insert model.Asset) (model.Asset, error) { return m, err } -func (a asset[T]) GetName(name string) (model.Asset, error) { +func (a asset[T]) GetByName(name string) (model.Asset, error) { m := model.Asset{} err := a.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE name = $1", a.table), name) if err != nil && err == sql.ErrNoRows { diff --git a/pkg/repository/network.go b/pkg/repository/network.go index d41faaca..c387c19c 100644 --- a/pkg/repository/network.go +++ b/pkg/repository/network.go @@ -13,7 +13,7 @@ type Network interface { Transactable Create(model.Network) (model.Network, error) GetById(id string) (model.Network, error) - GetChainID(chainId uint64) (model.Network, error) + GetByChainId(chainId uint64) (model.Network, error) Update(ID string, updates any) error } @@ -47,7 +47,7 @@ func (n network[T]) Create(insert model.Network) (model.Network, error) { return m, nil } -func (n network[T]) GetChainID(chainId uint64) (model.Network, error) { +func (n network[T]) GetByChainId(chainId uint64) (model.Network, error) { m := model.Network{} err := n.store.Get(&m, fmt.Sprintf("SELECT * FROM %s WHERE chain_id = $1", n.table), chainId) if err != nil && err == sql.ErrNoRows { diff --git a/pkg/service/chain.go b/pkg/service/chain.go index 4e1fd2df..fd252a6a 100644 --- a/pkg/service/chain.go +++ b/pkg/service/chain.go @@ -24,7 +24,7 @@ func stringFee(chainId uint64) (float64, error) { } func ChainInfo(chainId uint64, networkRepo repository.Network, assetRepo repository.Asset) (Chain, error) { - network, err := networkRepo.GetChainID(chainId) + network, err := networkRepo.GetByChainId(chainId) if err != nil { return Chain{}, common.StringError(err) } diff --git a/pkg/service/executor.go b/pkg/service/executor.go index 51d9b0b5..cbc5ea6d 100644 --- a/pkg/service/executor.go +++ b/pkg/service/executor.go @@ -39,7 +39,7 @@ type Executor interface { Estimate(call ContractCall) (CallEstimate, error) TxWait(txID string) (uint64, error) Close() error - GetChainID() (uint64, error) + GetByChainId() (uint64, error) GetBalance() (float64, error) } @@ -245,7 +245,7 @@ func (e executor) TxWait(txID string) (uint64, error) { return receipt.GasUsed, nil } -func (e executor) GetChainID() (uint64, error) { +func (e executor) GetByChainId() (uint64, error) { // Get ChainID from state var chainId64 uint64 err := e.client.Call(eth.ChainID().Returns(&chainId64)) diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 936fe7fa..59fe6a28 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -370,7 +370,7 @@ func (t transaction) populateInitialTxModelData(e model.ExecutionRequest, m *mod contractFunc := e.CxFunc + e.CxReturn m.ContractFunc = &contractFunc - asset, err := t.repos.Asset.GetName("USD") + asset, err := t.repos.Asset.GetByName("USD") if err != nil { return model.Asset{}, common.StringError(err) } @@ -401,7 +401,7 @@ func (t transaction) testTransaction(executor Executor, request model.Transactio wei := gas.Add(&estimateEVM.Value, gas) eth := common.WeiToEther(wei) - chainID, err := executor.GetChainID() + chainID, err := executor.GetByChainId() if err != nil { return res, eth, common.StringError(err) } @@ -644,7 +644,7 @@ func (t transaction) tenderTransaction(p transactionProcessingData) (float64, er profit := p.executionRequest.Quote.TotalUSD - trueUSD // Create Receive Tx leg - asset, err := t.repos.Asset.GetName("ETH") + asset, err := t.repos.Asset.GetById(p.chain.GasTokenID) if err != nil { return profit, common.StringError(err) } diff --git a/pkg/test/stubs/repository.go b/pkg/test/stubs/repository.go index a132be47..2dad2992 100644 --- a/pkg/test/stubs/repository.go +++ b/pkg/test/stubs/repository.go @@ -49,7 +49,7 @@ import ( // return model.Asset{}, nil // } -// func (Asset) GetName(name string) (model.Asset, error) { +// func (Asset) GetByName(name string) (model.Asset, error) { // if name == "AVAX" { // return avax, nil // } From 04ee50d9f22299b40b95217fea18e88f4450906a Mon Sep 17 00:00:00 2001 From: akfoster Date: Mon, 13 Feb 2023 19:17:10 -0600 Subject: [PATCH 35/35] Persist Checkout Capture Responses (#118) * need a payment_code on transactions to associate with checkout payment * persist paymentCode after capture; we may not be able to immediately get action data * make chargeCard() use transactionProcessingData struct * cleanup * remove encodedMessage from sign_test.go * remove log --- ...to-platform_device-to-instrument_platform.sql | 9 +++++++++ pkg/model/entity.go | 1 + pkg/model/request.go | 1 + pkg/service/checkout.go | 16 ++++++++++------ pkg/service/transaction.go | 6 ++++-- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql index f2220fdd..cc1036fa 100644 --- a/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql +++ b/migrations/0005_user-to-platform_contact-to-platform_device-to-instrument_platform.sql @@ -45,10 +45,19 @@ ALTER TABLE platform ADD COLUMN domains TEXT[] DEFAULT NULL, -- define which domains can make calls to API (web-to-API) ADD COLUMN ip_addresses TEXT[] DEFAULT NULL; -- define which API ips can make calls (API-to-API) +------------------------------------------------------------------------- +-- TRANSACTION ------------------------------------------------------------- +ALTER TABLE transaction + ADD COLUMN payment_code TEXT DEFAULT ''; ------------------------------------------------------------------------- -- +goose Down +------------------------------------------------------------------------- +-- TRANSACTION ------------------------------------------------------------- +ALTER TABLE transaction + DROP COLUMN IF EXISTS payment_code; + ------------------------------------------------------------------------- -- PLATFORM ------------------------------------------------------------- ALTER TABLE platform diff --git a/pkg/model/entity.go b/pkg/model/entity.go index bc8c0027..57d460fc 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -186,6 +186,7 @@ type Transaction struct { ProcessingFee string `json:"processingFee,omitempty" db:"processing_fee"` ProcessingFeeAsset string `json:"processingFeeAsset,omitempty" db:"processing_fee_asset"` StringFee string `json:"stringFee,omitempty" db:"string_fee"` + PaymentCode string `json:"paymentCode,omitempty" db:"payment_code"` } type AuthStrategy struct { diff --git a/pkg/model/request.go b/pkg/model/request.go index ca56c48e..f4701046 100644 --- a/pkg/model/request.go +++ b/pkg/model/request.go @@ -31,6 +31,7 @@ type TransactionUpdates struct { ProcessingFee *string `json:"processingFee" db:"processing_fee"` ProcessingFeeAsset *string `json:"processingFeeAsset" db:"processing_fee_asset"` StringFee *string `json:"stringFee" db:"string_fee"` + PaymentCode *string `json:"paymentCode" db:"payment_code"` } type InstrumentUpdates struct { diff --git a/pkg/service/checkout.go b/pkg/service/checkout.go index 62d8910f..da5fd7de 100644 --- a/pkg/service/checkout.go +++ b/pkg/service/checkout.go @@ -138,14 +138,14 @@ func AuthorizeCharge(p transactionProcessingData) (transactionProcessingData, er return p, nil } -func CaptureCharge(amount float64, userWallet string, authorizationID string) (capture *payments.CapturesResponse, err error) { +func CaptureCharge(p transactionProcessingData) (transactionProcessingData, error) { config, err := getConfig() if err != nil { - return nil, common.StringError(err) + return p, common.StringError(err) } client := payments.NewClient(*config) - usd := convertAmount(amount) + usd := convertAmount(p.executionRequest.Quote.TotalUSD) idempotencyKey := checkout.NewIdempotencyKey() params := checkout.Params{ @@ -155,11 +155,15 @@ func CaptureCharge(amount float64, userWallet string, authorizationID string) (c Amount: usd, } - capture, err = client.Captures(authorizationID, &request, ¶ms) + capture, err := client.Captures(p.cardAuthorization.AuthID, &request, ¶ms) if err != nil { - return nil, common.StringError(err) + return p, common.StringError(err) } + p.cardCapture = capture + + // TODO: call action, err = client.Actions(capture.Accepted.ActionID) in another service to check on + // TODO: Create entry for capture in our DB associated with userWallet - return capture, nil + return p, nil } diff --git a/pkg/service/transaction.go b/pkg/service/transaction.go index 59fe6a28..d4152ee1 100644 --- a/pkg/service/transaction.go +++ b/pkg/service/transaction.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/checkout/checkout-sdk-go/payments" "github.com/pkg/errors" "github.com/String-xyz/string-api/pkg/internal/common" @@ -59,6 +60,7 @@ type transactionProcessingData struct { chain *Chain executionRequest *model.ExecutionRequest cardAuthorization *AuthorizedCharge + cardCapture *payments.CapturesResponse preBalance *float64 recipientWalletId *string txId *string @@ -676,7 +678,7 @@ func (t transaction) tenderTransaction(p transactionProcessingData) (float64, er } func (t transaction) chargeCard(p transactionProcessingData) error { - _, err := CaptureCharge(p.executionRequest.Quote.TotalUSD, p.executionRequest.UserAddress, p.cardAuthorization.AuthID) + p, err := CaptureCharge(p) if err != nil { return common.StringError(err) } @@ -695,7 +697,7 @@ func (t transaction) chargeCard(p transactionProcessingData) error { if err != nil { return common.StringError(err) } - txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID} + txLeg := model.TransactionUpdates{ReceiptTxLegID: &receiptLeg.ID, PaymentCode: &p.cardCapture.Accepted.ActionID} err = t.repos.Transaction.Update(p.transactionModel.ID, txLeg) if err != nil { return common.StringError(err)