diff --git a/Dockerfile.debug b/Dockerfile.debug new file mode 100644 index 000000000..a06088fbb --- /dev/null +++ b/Dockerfile.debug @@ -0,0 +1,33 @@ +# Use the golang image for building and debugging +FROM golang:1.22.1-bullseye AS build +ARG GIT_COMMIT + +# Set the working directory inside the Docker container +WORKDIR /app + +# Copy the Go module files and download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire project directory from your host machine to the Docker container +COPY . ./github.com/stellar/stellar-disbursement-platform + +# Build your application +WORKDIR /app/github.com/stellar/stellar-disbursement-platform +RUN go build -gcflags="all=-N -l" -o stellar-disbursement-platform + +#install jq for automating tenant owners +RUN apt-get update && apt-get install -y jq + +# Install Delve for debugging +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +# Ensure the binary has executable permissions +RUN chmod +x /app/github.com/stellar/stellar-disbursement-platform +RUN chmod +x /app/github.com/stellar/stellar-disbursement-platform/dev/scripts/add_test_users.sh + +# Expose the ports for your application +EXPOSE 8001 + +# Set the entrypoint to start Delve in headless mode for debugging +ENTRYPOINT ["/go/bin/dlv", "exec", "./stellar-disbursement-platform", "serve", "--headless", "--listen=:2345", "--api-version=2", "--log"] diff --git a/Dockerfile.wip b/Dockerfile.wip new file mode 100644 index 000000000..054afd5ba --- /dev/null +++ b/Dockerfile.wip @@ -0,0 +1,32 @@ +# Stage 1: Build the Go application +FROM golang:1.22.1-bullseye AS build +ARG GIT_COMMIT + +WORKDIR /src/stellar-disbursement-platform +ADD go.mod go.sum ./ +RUN go mod download +ADD . ./ +RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" . + +# Stage 2: Setup the development environment with Delve for debugging +FROM golang:1.22.1-bullseye AS development + +WORKDIR /app/github.com/stellar/stellar-disbursement-platform +# Copy the built executable and all source files for debugging +COPY --from=build /src/stellar-disbursement-platform /app/github.com/stellar/stellar-disbursement-platform +# Build a debug version of the binary +RUN go build -gcflags="all=-N -l" -o stellar-disbursement-platform . +# Install Delve +RUN go install github.com/go-delve/delve/cmd/dlv@latest +# Ensure the binary has executable permissions +RUN chmod +x /app/github.com/stellar/stellar-disbursement-platform/stellar-disbursement-platform +EXPOSE 8001 2345 +ENTRYPOINT ["/go/bin/dlv", "debug", "./stellar-disbursement-platform", "serve", "--headless", "--listen=:2345", "--api-version=2", "--log"] + +# Stage 3: Create a production image using Ubuntu +FROM ubuntu:22.04 AS production +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates +COPY --from=build /bin/stellar-disbursement-platform /app/stellar-disbursement-platform +EXPOSE 8001 +WORKDIR /app +ENTRYPOINT ["./stellar-disbursement-platform"] diff --git a/Dockerfile.wip2 b/Dockerfile.wip2 new file mode 100644 index 000000000..054afd5ba --- /dev/null +++ b/Dockerfile.wip2 @@ -0,0 +1,32 @@ +# Stage 1: Build the Go application +FROM golang:1.22.1-bullseye AS build +ARG GIT_COMMIT + +WORKDIR /src/stellar-disbursement-platform +ADD go.mod go.sum ./ +RUN go mod download +ADD . ./ +RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" . + +# Stage 2: Setup the development environment with Delve for debugging +FROM golang:1.22.1-bullseye AS development + +WORKDIR /app/github.com/stellar/stellar-disbursement-platform +# Copy the built executable and all source files for debugging +COPY --from=build /src/stellar-disbursement-platform /app/github.com/stellar/stellar-disbursement-platform +# Build a debug version of the binary +RUN go build -gcflags="all=-N -l" -o stellar-disbursement-platform . +# Install Delve +RUN go install github.com/go-delve/delve/cmd/dlv@latest +# Ensure the binary has executable permissions +RUN chmod +x /app/github.com/stellar/stellar-disbursement-platform/stellar-disbursement-platform +EXPOSE 8001 2345 +ENTRYPOINT ["/go/bin/dlv", "debug", "./stellar-disbursement-platform", "serve", "--headless", "--listen=:2345", "--api-version=2", "--log"] + +# Stage 3: Create a production image using Ubuntu +FROM ubuntu:22.04 AS production +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates +COPY --from=build /bin/stellar-disbursement-platform /app/stellar-disbursement-platform +EXPOSE 8001 +WORKDIR /app +ENTRYPOINT ["./stellar-disbursement-platform"] diff --git a/cmd/serve.go b/cmd/serve.go index 53e9a39f9..a9657a3fc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -145,6 +145,11 @@ func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOpti MessengerClient: o.ServeOpts.SMSMessengerClient, MaxInvitationSMSResendAttempts: int64(o.ServeOpts.MaxInvitationSMSResendAttempts), Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey, +<<<<<<< HEAD + CrashTrackerClient: o.ServeOpts.CrashTrackerClient.Clone(), + UseExternalID: o.ServeOpts.UseExternalID, +======= +>>>>>>> develop }), ) if err != nil { @@ -341,6 +346,14 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ FlagDefault: 3, Required: true, }, + { + Name: "use-external-id", + Usage: "Enable or disable the use of external ID in wallet deep links", + OptType: types.Bool, + ConfigKey: &serveOpts.UseExternalID, // Ensure ServeOptions has a UseExternalID field of type bool + FlagDefault: false, // Default value set to false. Do Not embed external_id in wallet deep links + Required: false, + }, { Name: "single-tenant-mode", Usage: "This option enables the Single Tenant Mode feature. In the case where multi-tenancy is not required, this options bypasses the tenant resolution by always resolving to the default tenant configured in the database.", @@ -672,6 +685,7 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ // Starting Application Server log.Ctx(ctx).Info("Starting Application Server...") + fmt.Printf("Use External ID: %t\n", serveOpts.UseExternalID) serverService.StartServe(serveOpts, &serve.HTTPServer{}) }, } diff --git a/compose-dev.yaml b/compose-dev.yaml new file mode 100644 index 000000000..a082bd9fe --- /dev/null +++ b/compose-dev.yaml @@ -0,0 +1,12 @@ +services: + app: + entrypoint: + - sleep + - infinity + image: docker/dev-environments-go:stable-1 + init: true + volumes: + - type: bind + source: /var/run/docker.sock + target: /var/run/docker.sock + diff --git a/dev/.env.old b/dev/.env.old new file mode 100644 index 000000000..d559af60f --- /dev/null +++ b/dev/.env.old @@ -0,0 +1,14 @@ +# Generate a new keypair for SEP-10 signing +SEP10_SIGNING_PUBLIC_KEY=GBQGDASIDGI6SAOWXPJXJZSAKPP5W3IJYU2YN5B4O33W3MVWNMKT2BYZ +SEP10_SIGNING_PRIVATE_KEY=SAFAFP3M4PHXRYJD6J4EA33MVZWGUEMALFKVROCEFVV6J32X7LZBIBYG + +# Generate a new keypair for the distribution account +DISTRIBUTION_PUBLIC_KEY=GCFOB7CATOKXZSUFI362MO6SRTJUWOCZWWMPF2LX3Q2Z5M5CHQ7JYKD2 +DISTRIBUTION_SEED=SDOHYDJARUVAGQ2KO6KLIOAYGK4CCQ5RLWMUNLAOVXGS2FX6PHWBK3JO + +# CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE +CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE=${DISTRIBUTION_SEED} + +# Distribution signer +DISTRIBUTION_SIGNER_TYPE=DISTRIBUTION_ACCOUNT_ENV +DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE=${DISTRIBUTION_SEED} diff --git a/dev/docker-compose-sdp-anchor.yml b/dev/docker-compose-sdp-anchor.yml index 98df021ba..ed9806f35 100644 --- a/dev/docker-compose-sdp-anchor.yml +++ b/dev/docker-compose-sdp-anchor.yml @@ -81,7 +81,6 @@ services: RECAPTCHA_SITE_SECRET_KEY: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe ANCHOR_PLATFORM_OUTGOING_JWT_SECRET: mySdpToAnchorPlatformSecret entrypoint: "./dev/scripts/debug_entrypoint.sh" - db-anchor-platform: container_name: anchor-platform-postgres-db-mtn image: postgres:14-alpine diff --git a/dev/docker-compose-sdp-anchor.yml.backup b/dev/docker-compose-sdp-anchor.yml.backup new file mode 100644 index 000000000..38c77c9e1 --- /dev/null +++ b/dev/docker-compose-sdp-anchor.yml.backup @@ -0,0 +1,245 @@ +version: '3.8' +services: + db: + container_name: sdp_v2_database-mtn + image: postgres:14-alpine + environment: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: sdp_mtn + PGDATA: /data/postgres + ports: + - "5432:5432" + volumes: + - postgres-db:/data/postgres + + sdp-api: + container_name: sdp-api-mtn + image: debug-sdp-api:latest + platform: linux/amd64 + build: + context: ../ + dockerfile: Dockerfile + ports: + - "8000:8000" + - "8002:8002" + - "8003:8003" + - "2345:2345" # Exposing the debug port for go applications + environment: + DEBUG: true + BASE_URL: http://localhost:8000 + DATABASE_URL: postgres://postgres@db:5432/sdp_mtn?sslmode=disable + ENVIRONMENT: localhost + LOG_LEVEL: TRACE + PORT: "8000" + METRICS_PORT: "8002" + METRICS_TYPE: PROMETHEUS + EMAIL_SENDER_TYPE: DRY_RUN + SMS_SENDER_TYPE: DRY_RUN + NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" + EC256_PUBLIC_KEY: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ3HNphPAEKHvtRjsl5Kjwc9tTMqS\n2pmYNybrLsxZ6cuQvg2yiEoXZixP2cJ77csHClXC6cb1wQp/BNGDvGKoPg==\n-----END PUBLIC KEY-----" + SEP10_SIGNING_PUBLIC_KEY: ${SEP10_SIGNING_PUBLIC_KEY} + ANCHOR_PLATFORM_BASE_SEP_URL: http://localhost:8080 + ANCHOR_PLATFORM_BASE_PLATFORM_URL: http://anchor-platform:8085 + DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY} + DISTRIBUTION_SEED: ${DISTRIBUTION_SEED} + RECAPTCHA_SITE_KEY: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI + DISABLE_MFA: "true" + DISABLE_RECAPTCHA: "true" + CORS_ALLOWED_ORIGINS: "*" + + # multi-tenant + ADMIN_PORT: "8003" + INSTANCE_NAME: "SDP Testnet on Docker" + TENANT_XLM_BOOTSTRAP_AMOUNT: 5 + DISTRIBUTION_SIGNER_TYPE: ${DISTRIBUTION_SIGNER_TYPE:-DISTRIBUTION_ACCOUNT_ENV} + SINGLE_TENANT_MODE: "false" + + # scheduler options + ENABLE_SCHEDULER: "true" # disabled - we're using kafka for this. + SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS: "10" + SCHEDULER_PAYMENT_JOB_SECONDS: "10" + + # Broker options + EVENT_BROKER_TYPE: "NONE" + BROKER_URLS: "kafka:9092" + CONSUMER_GROUP_ID: "group-id" + KAFKA_SECURITY_PROTOCOL: "PLAINTEXT" + + # multi-tenant secrets: + ADMIN_ACCOUNT: SDP-admin + ADMIN_API_KEY: api_key_1234567890 + DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE: ${DISTRIBUTION_ACCOUNT_ENCRYPTION_PASSPHRASE} + CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE: ${CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE} + + # secrets: + AWS_ACCESS_KEY_ID: MY_AWS_ACCESS_KEY_ID + AWS_REGION: MY_AWS_REGION + AWS_SECRET_ACCESS_KEY: MY_AWS_SECRET_ACCESS_KEY + AWS_SES_SENDER_ID: MY_AWS_SES_SENDER_ID + TWILIO_ACCOUNT_SID: MY_TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN: MY_TWILIO_AUTH_TOKEN + TWILIO_SERVICE_SID: MY_TWILIO_SERVICE_SID + EC256_PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdo6o+tdFkF94B7z8\nnoybH6/zO3PryLLjLbj54/zOi4WhRANCAAQncc2mE8AQoe+1GOyXkqPBz21MypLa\nmZg3JusuzFnpy5C+DbKIShdmLE/ZwnvtywcKVcLpxvXBCn8E0YO8Yqg+\n-----END PRIVATE KEY-----" + SEP10_SIGNING_PRIVATE_KEY: ${SEP10_SIGNING_PRIVATE_KEY} + SEP24_JWT_SECRET: jwt_secret_1234567890 + RECAPTCHA_SITE_SECRET_KEY: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe + ANCHOR_PLATFORM_OUTGOING_JWT_SECRET: mySdpToAnchorPlatformSecret + entrypoint: "" + command: + - sh + - -c + - | + sleep 5 + ./stellar-disbursement-platform db admin migrate up + ./stellar-disbursement-platform db tss migrate up + ./stellar-disbursement-platform db auth migrate up --all + ./stellar-disbursement-platform db sdp migrate up --all + ./stellar-disbursement-platform db setup-for-network --all + /usr/local/bin/dlv debug ./stellar-disbursement-platform serve --api-version=2 --listen=0.0.0.0:2345 --headless=true --log + #./stellar-disbursement-platform serve + + db-anchor-platform: + container_name: anchor-platform-postgres-db-mtn + image: postgres:14-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + PGPORT: 5433 + ports: + - "5433:5433" + volumes: + - postgres-ap-db:/data/postgres + + anchor-platform: + container_name: anchor-platform + image: stellar/anchor-platform:2.6.0 + command: --sep-server --platform-server --platform linux/amd64 + ports: + - "8080:8080" # sep-server + - "8085:8085" # platform-server + - "8082:8082" # metrics + environment: + HOST_URL: http://localhost:8080 + SEP_SERVER_PORT: 8080 + CALLBACK_API_BASE_URL: http://sdp-api:8000 + CALLBACK_API_AUTH_TYPE: none # TODO: update to jwt later + PLATFORM_SERVER_AUTH_TYPE: JWT + APP_LOGGING_LEVEL: INFO + DATA_TYPE: postgres + DATA_SERVER: db-anchor-platform:5433 + DATA_DATABASE: postgres + DATA_FLYWAY_ENABLED: "true" + DATA_DDL_AUTO: update + METRICS_ENABLED: "false" # Metrics would be available at port 8082 + METRICS_EXTRAS_ENABLED: "false" + SEP10_ENABLED: "true" + SEP10_HOME_DOMAINS: "localhost:8000, *.stellar.local:8000" # Comma separated list of home domains + SEP10_HOME_DOMAIN: "" + SEP10_WEB_AUTH_DOMAIN: "localhost:8080" + # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true # RECOMMENDED + # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com" # RECOMMENDED + SEP24_ENABLED: "true" + SEP24_INTERACTIVE_URL_BASE_URL: http://localhost:8000/wallet-registration/start + SEP24_INTERACTIVE_URL_JWT_EXPIRATION: 1800 # 1800 seconds is 30 minutes + SEP24_MORE_INFO_URL_BASE_URL: http://localhost:8000/wallet-registration/start + SEP1_ENABLED: "true" + SEP1_TOML_TYPE: url + SEP1_TOML_VALUE: http://sdp-api:8000/.well-known/stellar.toml + ASSETS_TYPE: json + ASSETS_VALUE: | # TODO: keep this up to date with the latest assets supported by the SDP + { + "assets": [ + { + "sep24_enabled": true, + "schema": "stellar", + "code": "USDC", + "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + "distribution_account": "${DISTRIBUTION_PUBLIC_KEY}", + "significant_decimals": 7, + "deposit": { + "enabled": true, + "fee_minimum": 0, + "fee_percent": 0, + "min_amount": 1, + "max_amount": 10000 + }, + "withdraw": {"enabled": false} + }, + { + "sep24_enabled": true, + "schema": "stellar", + "code": "native", + "issuer": "", + "distribution_account": "${DISTRIBUTION_PUBLIC_KEY}", + "significant_decimals": 7, + "deposit": { + "enabled": true, + "fee_minimum": 0, + "fee_percent": 0, + "min_amount": 1, + "max_amount": 10000 + }, + "withdraw": {"enabled": false} + } + ] + } + + # secrets: + SECRET_DATA_USERNAME: postgres + SECRET_DATA_PASSWORD: postgres + SECRET_PLATFORM_API_AUTH_SECRET: mySdpToAnchorPlatformSecret + SECRET_SEP10_JWT_SECRET: jwt_secret_1234567890 + SECRET_SEP10_SIGNING_SEED: ${SEP10_SIGNING_PRIVATE_KEY} + SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET: jwt_secret_1234567890 + SECRET_SEP24_MORE_INFO_URL_JWT_SECRET: jwt_secret_1234567890 + + kafka: + image: docker.io/bitnami/kafka:3.6 + ports: + - "9094:9094" + - "9092:9092" + volumes: + - "kafka-data:/bitnami" + environment: + # KRaft settings + - KAFKA_CFG_NODE_ID=0 + - KAFKA_CFG_PROCESS_ROLES=controller,broker + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 + # Listeners + - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + healthcheck: + test: kafka-topics.sh --bootstrap-server kafka:9092 --list || exit -1 + start_period: 10s + interval: 10s + timeout: 10s + retries: 5 + + kafka-init: + image: docker.io/bitnami/kafka:3.6 + entrypoint: [ "/bin/bash", "-c" ] + command: | + " + kafka-topics.sh --create --if-not-exists --topic events.receiver-wallets.new_invitation --bootstrap-server kafka:9092 + kafka-topics.sh --create --if-not-exists --topic events.payment.payment_completed --bootstrap-server kafka:9092 + kafka-topics.sh --create --if-not-exists --topic events.payment.ready_to_pay --bootstrap-server kafka:9092 + " + demo-wallet: + build: + context: . + dockerfile: Dockerfile-demo-wallet + ports: + - "4000:80" + volumes: + - ./env-config-demo-wallet.js:/usr/share/nginx/html/settings/env-config.js + +volumes: + postgres-db: + driver: local + postgres-ap-db: + driver: local + kafka-data: + driver: local diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index e35a384f3..cbbd1f44f 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: db: extends: @@ -13,10 +12,10 @@ services: depends_on: db: condition: service_started - kafka: - condition: service_started - kafka-init: - condition: service_completed_successfully + #kafka: + # condition: service_started + #kafka-init: + # condition: service_completed_successfully db-anchor-platform: extends: file: docker-compose-sdp-anchor.yml @@ -38,7 +37,7 @@ services: depends_on: - db - sdp-api - - kafka +# - kafka sdp-frontend: extends: file: docker-compose-frontend.yml @@ -46,19 +45,19 @@ services: depends_on: - db - sdp-api - kafka: - extends: - file: docker-compose-sdp-anchor.yml - service: kafka - volumes: - - kafka-data:/bitnami - kafka-init: - extends: - file: docker-compose-sdp-anchor.yml - service: kafka-init - depends_on: - kafka: - condition: service_healthy + #kafka: + # extends: + # file: docker-compose-sdp-anchor.yml + # service: kafka + # volumes: + # - kafka-data:/bitnami + #kafka-init: + # extends: + # file: docker-compose-sdp-anchor.yml + # service: kafka-init + # depends_on: + # kafka: + # condition: service_healthy demo-wallet: extends: file: docker-compose-sdp-anchor.yml diff --git a/dev/main.sh b/dev/main.sh index 15d6699ce..2c10d9a20 100755 --- a/dev/main.sh +++ b/dev/main.sh @@ -74,6 +74,7 @@ echo $DIVIDER echo "====> โœ…finish calling docker-compose up" + # Initialize tenants echo $DIVIDER echo "====> ๐Ÿ‘€Step 3: initialize tenants... (๐Ÿ˜ด 10s sleep)" diff --git a/dev/sample/SDP.postman_collection.json b/dev/sample/SDP.postman_collection.json new file mode 100644 index 000000000..d43efe100 --- /dev/null +++ b/dev/sample/SDP.postman_collection.json @@ -0,0 +1,223 @@ +{ + "info": { + "_postman_id": "e32adb9c-7e41-4138-b3b7-7f25d6f11995", + "name": "SDP", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "20979969" + }, + "item": [ + { + "name": "/tenants", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + }, + { + "name": "/tenants/default-tenant", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "SDP-admin", + "type": "string" + }, + { + "key": "password", + "value": "api_key_1234567890", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"a2e9ba7c-ec70-4d06-b36a-0f2ec805f0b4\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8003/tenants/default-tenant", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8003", + "path": [ + "tenants", + "default-tenant" + ] + } + }, + "response": [] + }, + { + "name": "/tenants", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "SDP-admin", + "type": "string" + }, + { + "key": "password", + "value": "api_key_1234567890", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": " {\n \"sdp_ui_base_url\": \"https://sdp-backend-dashboard-pr256.previews.kube001.services.stellar-ops.com\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8003/tenants/a2e9ba7c-ec70-4d06-b36a-0f2ec805f0b4", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8003", + "path": [ + "tenants", + "a2e9ba7c-ec70-4d06-b36a-0f2ec805f0b4" + ] + } + }, + "response": [] + }, + { + "name": "/tenants", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "SDP-admin", + "type": "string" + }, + { + "key": "password", + "value": "api_key_1234567890", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"bluecorp\",\n \"organization_name\": \"bluecorp\",\n \"email_sender_type\": \"AWS_EMAIL\",\n \"sms_sender_type\": \"TWILIO_SMS\",\n \"base_url\": \"https://sdp-backend-pr256.previews.kube001.services.stellar-ops.com\",\n \"sdp_ui_base_url\": \"https://sdp-backend-dashboard-pr256.previews.kube001.services.stellar-ops.com\",\n \"owner_email\": \"reece@stellar.org\",\n \"owner_first_name\": \"jane\",\n \"owner_last_name\": \"doe\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8003/tenants", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8003", + "path": [ + "tenants" + ] + } + }, + "response": [ + { + "name": "/tenants", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"bluecorp\",\n \"organization_name\": \"bluecorp\",\n \"email_sender_type\": \"AWS_EMAIL\",\n \"sms_sender_type\": \"TWILIO_SMS\",\n \"base_url\": \"https://sdp-backend-pr256.previews.kube001.services.stellar-ops.com\",\n \"sdp_ui_base_url\": \"https://sdp-backend-dashboard-pr256.previews.kube001.services.stellar-ops.com\",\n \"owner_email\": \"reece@stellar.org\",\n \"owner_first_name\": \"jane\",\n \"owner_last_name\": \"doe\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8003/tenants", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8003", + "path": [ + "tenants" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Content-Disposition", + "value": "inline" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Date", + "value": "Sat, 27 Apr 2024 15:36:07 GMT" + }, + { + "key": "Content-Length", + "value": "567" + } + ], + "cookie": [], + "body": "{\n \"id\": \"944e5748-693f-483a-be6d-e1bebb875899\",\n \"name\": \"bluecorp\",\n \"email_sender_type\": \"AWS_EMAIL\",\n \"sms_sender_type\": \"TWILIO_SMS\",\n \"base_url\": \"https://sdp-backend-pr256.previews.kube001.services.stellar-ops.com\",\n \"sdp_ui_base_url\": \"https://sdp-backend-dashboard-pr256.previews.kube001.services.stellar-ops.com\",\n \"status\": \"TENANT_PROVISIONED\",\n \"distribution_account\": \"GBC2HVWFIFN7WJHFORVBCDKJORG6LWTW3O2QBHOURL3KHZPM4KMWTUSA\",\n \"is_default\": false,\n \"created_at\": \"2024-04-27T15:36:06.839203Z\",\n \"updated_at\": \"2024-04-27T15:36:07.992456Z\"\n}" + } + ] + } + ], + "auth": { + "type": "bearer" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/dev/scripts/create_and_fund b/dev/scripts/create_and_fund new file mode 100755 index 000000000..2c3dc45b7 Binary files /dev/null and b/dev/scripts/create_and_fund differ diff --git a/dev/scripts/create_and_fund.go.back b/dev/scripts/create_and_fund.go.back new file mode 100644 index 000000000..57f233d54 --- /dev/null +++ b/dev/scripts/create_and_fund.go.back @@ -0,0 +1,108 @@ +# create stellar wallet wallet, fund xlm, add/fund usdc with trustline @reecemarkowsky +package main + +import ( + "flag" + "fmt" + "log" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" +) + +var pair *keypair.Full +var err error + +if secretKey == "" { + // Do not redeclare pair, just assign + pair, err = keypair.Random() + if err != nil { + log.Fatalf("Failed to generate a new keypair: %v", err) + } + } else { + // Do not redeclare pair, just assign + pair, err = keypair.ParseFull(secretKey) + if err != nil { + log.Fatalf("Failed to parse secret key: %v", err) + } + } + + // Ensure that pair is not nil before using it + if pair != nil { + fmt.Printf("Public Key: %s\n", pair.Address()) + fmt.Printf("Secret Key: %s\n", pair.Seed()) + } else { + log.Fatal("Key pair was not initialized.") + } + + client := horizonclient.DefaultTestNetClient + + // Fund with XLM using Friendbot if --fundxlm is specified + if fundXLM { + _, err = client.Fund(pair.Address()) + if err != nil { + log.Fatalf("Failed to fund with Friendbot: %v", err) + } + } + + accRequest := horizonclient.AccountRequest{AccountID: pair.Address()} + sourceAccount, err := client.AccountDetail(accRequest) + if err != nil { + log.Fatalf("Failed to get account details: %v", err) + } + + // Fund with USDC using a DEX buy offer if --fundusdc is specified + if fundUSDC { + // Define the USDC asset and convert it to a ChangeTrustAsset + usdcAsset := txnbuild.CreditAsset{Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"} + changeTrustAsset, err := usdcAsset.ToChangeTrustAsset() + if err != nil { + log.Fatalf("Failed to convert to ChangeTrustAsset: %v", err) + } + + trustLine := txnbuild.ChangeTrust{ + Line: changeTrustAsset, + } + + price := xdr.Price{N: 1, D: 2} // This creates a price of 0.5 (1/2) + + buyOffer := txnbuild.ManageBuyOffer{ + Selling: &txnbuild.NativeAsset{}, // Selling XLM + Buying: &usdcAsset, // Buying USDC + Amount: "10", // Amount of USDC you want to buy + Price: price, // Price of 1 USDC in terms of XLM + OfferID: 0, // Set to 0 to create a new offer + } + + // Add this buy offer to your transaction + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{&trustLine, &buyOffer}, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, + }, + ) + + if err != nil { + log.Fatalf("Failed to build the transaction: %v", err) + } + + tx, err = tx.Sign(network.TestNetworkPassphrase, pair) + if err != nil { + log.Fatalf("Failed to sign the transaction: %v", err) + } + + resp, err := client.SubmitTransaction(tx) + if err != nil { + log.Fatalf("Failed to submit the buy offer transaction: %v", err) + } + fmt.Printf("Buy offer transaction successful: %s\n", resp.Hash) + } + + // Additional transaction logic can be added below as needed +} diff --git a/dev/scripts/sep24_deposit.go b/dev/scripts/sep24_deposit.go new file mode 100644 index 000000000..c240cdbf4 --- /dev/null +++ b/dev/scripts/sep24_deposit.go @@ -0,0 +1,215 @@ +package main + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/BurntSushi/toml" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" +) + +const ( + tomlPath = "/.well-known/stellar.toml" +) + +type StellarTOML struct { + WEB_AUTH_ENDPOINT string `toml:"WEB_AUTH_ENDPOINT"` + TRANSFER_SERVER_SEP string `toml:"TRANSFER_SERVER_SEP0024"` +} + +func main() { + if len(os.Args) != 3 { + fmt.Println("Usage: go run script.go [domain] [secretKey]") + os.Exit(1) + } + + domain := os.Args[1] + secretKey := os.Args[2] + + // Fetch and parse the stellar.toml file + config, err := fetchStellarTOML(domain) + + if err != nil { + fmt.Println("Failed to fetch or parse stellar.toml:", err) + return + } + fmt.Println("Fetched stellar.toml successfully:", config) + print (config.WEB_AUTH_ENDPOINT) + + kp, err := keypair.ParseFull(secretKey) + if err != nil { + fmt.Println("Failed to parse secret key:", err) + return + } + fmt.Println("Successfully parsed secret key, public key is:", kp.Address()) + fmt.Println("AUTH SERVER:", config.WEB_AUTH_ENDPOINT) + + token, err := performSEP10Auth(config.WEB_AUTH_ENDPOINT, kp) + if err != nil { + fmt.Println("SEP-10 Authentication failed:", err) + return + } + fmt.Println("SEP-10 Authentication successful, token obtained:", token) + + err = performSEP24Deposit(config.TRANSFER_SERVER_SEP, token, kp.Address()) + if err != nil { + fmt.Println("SEP-24 Deposit failed:", err) + return + } + fmt.Println("SEP-24 Deposit initiated successfully") +} + +func fetchStellarTOML(domain string) (StellarTOML, error) { + var config StellarTOML + tomlURL := "http://" + domain + tomlPath + print (tomlURL) + + // Create a client to skip certificate verification (for testing) + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + resp, err := client.Get(tomlURL) + if err != nil { + return config, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + return config, fmt.Errorf("failed to fetch stellar.toml: %s, response: %s", tomlURL, string(bodyBytes)) + } + + _, err = toml.DecodeReader(resp.Body, &config) + return config, err +} + +func performSEP10Auth(authURL string, kp *keypair.Full) (string, error) { + fmt.Println("Fetching challenge transaction from:", authURL) + + // request a transaction to sign + resp, err := http.Get(authURL + "?account=" + kp.Address()) + if err != nil { + return "", err + } + defer resp.Body.Close() + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch challenge: status code %d, response: %s", resp.StatusCode, string(bodyBytes)) + } + + var challenge struct { + Transaction string `json:"transaction"` + Network string `json:"network"` + } + + if err := json.Unmarshal(bodyBytes, &challenge); err != nil { + return "", err + } + + // Parse the transaction from XDR + genericTx, err := txnbuild.TransactionFromXDR(challenge.Transaction) + if err != nil { + return "", fmt.Errorf("unable to parse challenge transaction: %v", err) + } + + // Check if it's a regular transaction and unwrap it + tx, ok := genericTx.Transaction() + if !ok { + return "", fmt.Errorf("parsed data is not a regular transaction") + } + + // Sign the transaction + networkPassphrase := network.TestNetworkPassphrase // or network.PublicNetworkPassphrase for production + signedTx, err := tx.Sign(networkPassphrase, kp) + if err != nil { + return "", fmt.Errorf("unable to sign challenge transaction: %v", err) + } + + // Convert the signed transaction to base64 XDR format to be ready for submission + signedTxXDR, err := signedTx.Base64() + if err != nil { + return "", fmt.Errorf("unable to convert signed transaction to base64 XDR: %v", err) + } + + // Submit the signed transaction + reqBody, err := json.Marshal(map[string]string{"transaction": signedTxXDR}) + if err != nil { + return "", err + } + + verifyResp, err := http.Post(authURL, "application/json", bytes.NewBuffer(reqBody)) + if err != nil { + return "", err + } + defer verifyResp.Body.Close() + + verifyBodyBytes, _ := ioutil.ReadAll(verifyResp.Body) + if verifyResp.StatusCode != http.StatusOK { + return "", fmt.Errorf("verification failed: status code %d, response: %s", verifyResp.StatusCode, string(verifyBodyBytes)) + } + + var authResponse struct { + Token string `json:"token"` + } + + if err := json.Unmarshal(verifyBodyBytes, &authResponse); err != nil { + return "", fmt.Errorf("error parsing verification response: %v", err) + } + + return authResponse.Token, nil +} + +func performSEP24Deposit(depositURL, token, account string) error { + params := map[string]string{ + "account": account, + "asset_code": "USDC", + "lang": "en", + "claimable_balance_supported": "false", + } + jsonData, err := json.Marshal(params) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", depositURL + "/transactions/deposit/interactive", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed deposit: %s", string(body)) + } + + fmt.Println("Deposit response status:", resp.Status) + fmt.Println("Deposit response body:", string(body)) + return nil +} diff --git a/dev/scripts/stellar.toml b/dev/scripts/stellar.toml new file mode 100644 index 000000000..ccdaa4493 --- /dev/null +++ b/dev/scripts/stellar.toml @@ -0,0 +1,24 @@ +ACCOUNTS=["GDFSZFORVKOAGHJVLE7OJTM44KR2NYICFIYZ7HUES3ULUYQVD4PX5B6O", "GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4"] +SIGNING_KEY="GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4" +NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +HORIZON_URL="https://horizon-testnet.stellar.org" +WEB_AUTH_ENDPOINT="http://localhost:8080/auth" +TRANSFER_SERVER_SEP0024="http://localhost:8080/sep24" + +[DOCUMENTATION] +ORG_NAME="redcorp" + +[[CURRENCIES]] +code = "USDC" +issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" +is_asset_anchored = true +anchor_asset_type = "fiat" +status = "live" +desc = "USDC" + +[[CURRENCIES]] +code = "native" +status = "live" +is_asset_anchored = true +anchor_asset_type = "crypto" +desc = "XLM, the native token of the Stellar Network." \ No newline at end of file diff --git a/dev/scripts/stellar.toml.1 b/dev/scripts/stellar.toml.1 new file mode 100644 index 000000000..ccdaa4493 --- /dev/null +++ b/dev/scripts/stellar.toml.1 @@ -0,0 +1,24 @@ +ACCOUNTS=["GDFSZFORVKOAGHJVLE7OJTM44KR2NYICFIYZ7HUES3ULUYQVD4PX5B6O", "GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4"] +SIGNING_KEY="GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4" +NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +HORIZON_URL="https://horizon-testnet.stellar.org" +WEB_AUTH_ENDPOINT="http://localhost:8080/auth" +TRANSFER_SERVER_SEP0024="http://localhost:8080/sep24" + +[DOCUMENTATION] +ORG_NAME="redcorp" + +[[CURRENCIES]] +code = "USDC" +issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" +is_asset_anchored = true +anchor_asset_type = "fiat" +status = "live" +desc = "USDC" + +[[CURRENCIES]] +code = "native" +status = "live" +is_asset_anchored = true +anchor_asset_type = "crypto" +desc = "XLM, the native token of the Stellar Network." \ No newline at end of file diff --git a/dev/scripts/stellar.toml.2 b/dev/scripts/stellar.toml.2 new file mode 100644 index 000000000..ccdaa4493 --- /dev/null +++ b/dev/scripts/stellar.toml.2 @@ -0,0 +1,24 @@ +ACCOUNTS=["GDFSZFORVKOAGHJVLE7OJTM44KR2NYICFIYZ7HUES3ULUYQVD4PX5B6O", "GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4"] +SIGNING_KEY="GAH74OK3PJ76LJQ5DH5QMWUL3UJ5D2JCNLQ7LXHUQHXLEBXER2DWYOB4" +NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +HORIZON_URL="https://horizon-testnet.stellar.org" +WEB_AUTH_ENDPOINT="http://localhost:8080/auth" +TRANSFER_SERVER_SEP0024="http://localhost:8080/sep24" + +[DOCUMENTATION] +ORG_NAME="redcorp" + +[[CURRENCIES]] +code = "USDC" +issuer = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" +is_asset_anchored = true +anchor_asset_type = "fiat" +status = "live" +desc = "USDC" + +[[CURRENCIES]] +code = "native" +status = "live" +is_asset_anchored = true +anchor_asset_type = "crypto" +desc = "XLM, the native token of the Stellar Network." \ No newline at end of file diff --git a/dev/scripts/test.sh b/dev/scripts/test.sh new file mode 100755 index 000000000..fed27a3af --- /dev/null +++ b/dev/scripts/test.sh @@ -0,0 +1,5 @@ +eval $(go run create_and_fund.go) + +# Now the environment variables are set in the shell +echo $STELLAR_PUBLIC_KEY +echo $STELLAR_SECRET_KEY diff --git a/helmchart/.DS_Store b/helmchart/.DS_Store new file mode 100644 index 000000000..4912e8899 Binary files /dev/null and b/helmchart/.DS_Store differ diff --git a/internal/data/assets.go b/internal/data/assets.go index 723f80df5..b987c689e 100644 --- a/internal/data/assets.go +++ b/internal/data/assets.go @@ -286,6 +286,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal r.id AS "receiver_wallet.receiver.id", r.phone_number AS "receiver_wallet.receiver.phone_number", r.email AS "receiver_wallet.receiver.email", + r.external_id AS "receiver_wallet.receiver.external_id", a.id AS "asset.id", a.code AS "asset.code", a.issuer AS "asset.issuer", diff --git a/internal/data/assets_test.go b/internal/data/assets_test.go index fd4b9fd07..b0c392b51 100644 --- a/internal/data/assets_test.go +++ b/internal/data/assets_test.go @@ -640,6 +640,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverX.ID, Email: receiverX.Email, PhoneNumber: receiverX.PhoneNumber, + ExternalID: receiverX.ExternalID, }, ReceiverWalletStats: ReceiverWalletStats{ TotalInvitationSMSResentAttempts: 2, @@ -657,6 +658,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverX.ID, Email: receiverX.Email, PhoneNumber: receiverX.PhoneNumber, + ExternalID: receiverX.ExternalID, }, InvitationSentAt: &invitationSentAt, }, @@ -671,6 +673,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverX.ID, Email: receiverX.Email, PhoneNumber: receiverX.PhoneNumber, + ExternalID: receiverX.ExternalID, }, }, WalletID: walletB.ID, @@ -684,6 +687,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverX.ID, Email: receiverX.Email, PhoneNumber: receiverX.PhoneNumber, + ExternalID: receiverX.ExternalID, }, }, WalletID: walletB.ID, @@ -697,6 +701,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverY.ID, Email: receiverY.Email, PhoneNumber: receiverY.PhoneNumber, + ExternalID: receiverY.ExternalID, }, }, WalletID: walletA.ID, @@ -710,6 +715,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverY.ID, Email: receiverY.Email, PhoneNumber: receiverY.PhoneNumber, + ExternalID: receiverY.ExternalID, }, }, WalletID: walletA.ID, @@ -723,6 +729,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverY.ID, Email: receiverY.Email, PhoneNumber: receiverY.PhoneNumber, + ExternalID: receiverY.ExternalID, }, }, WalletID: walletB.ID, @@ -736,6 +743,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { ID: receiverY.ID, Email: receiverY.Email, PhoneNumber: receiverY.PhoneNumber, + ExternalID: receiverY.ExternalID, }, }, WalletID: walletB.ID, diff --git a/internal/data/receivers.go b/internal/data/receivers.go index 9beb4a3ac..0e88991d5 100644 --- a/internal/data/receivers.go +++ b/internal/data/receivers.go @@ -31,6 +31,9 @@ type ReceiverRegistrationRequest struct { VerificationValue string `json:"verification"` VerificationType VerificationField `json:"verification_type"` ReCAPTCHAToken string `json:"recaptcha_token"` + ExternalID string `json:"external_id"` + CustomerID string `json:"customer_id"` // customer identifier + MobileNumberHash string `json:"mobile_number"` // hashed mobile number } type ReceiverStats struct { @@ -443,3 +446,25 @@ func (r *ReceiverModel) DeleteByPhoneNumber(ctx context.Context, dbConnectionPoo return nil }) } + +// GetByExternalID retrieves a receiver's phone number based on the external_id. +func (r *ReceiverModel) GetByExternalID(ctx context.Context, sqlExec db.SQLExecuter, externalID string) (*Receiver, error) { + receiver := Receiver{} + + query := ` + SELECT id, phone_number + FROM receivers + WHERE external_id = $1 + LIMIT 1 + ` + + err := sqlExec.GetContext(ctx, &receiver, query, externalID) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("no receiver found with external_id %s: %w", externalID, err) + } + // Handle other potential errors + return nil, fmt.Errorf("error fetching receiver by external ID: %w", err) + } + return &receiver, nil +} diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go index 555e699f2..b2eb3df1b 100644 --- a/internal/data/receivers_wallet.go +++ b/internal/data/receivers_wallet.go @@ -231,6 +231,7 @@ const getPendingRegistrationReceiverWalletsBaseQuery = ` r.id AS "receiver.id", r.phone_number AS "receiver.phone_number", r.email AS "receiver.email", + r.external_id AS "receiver.external_id", w.id AS "wallet.id", w.name AS "wallet.name" FROM diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go index ba229a8a6..26067e760 100644 --- a/internal/data/receivers_wallet_test.go +++ b/internal/data/receivers_wallet_test.go @@ -893,6 +893,7 @@ func Test_ReceiverWallet_GetAllPendingRegistration(t *testing.T) { ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet3.ID, @@ -905,6 +906,7 @@ func Test_ReceiverWallet_GetAllPendingRegistration(t *testing.T) { ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet4.ID, @@ -1014,6 +1016,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByReceiverWalletIDs(t *testing ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet3.ID, @@ -1026,6 +1029,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByReceiverWalletIDs(t *testing ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet4.ID, @@ -1097,6 +1101,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByReceiverWalletIDs(t *testing ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet1.ID, @@ -1109,6 +1114,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByReceiverWalletIDs(t *testing ID: receiver.ID, PhoneNumber: receiver.PhoneNumber, Email: receiver.Email, + ExternalID: receiver.ExternalID, }, Wallet: Wallet{ ID: wallet2.ID, @@ -1199,6 +1205,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByDisbursementID(t *testing.T) ID: receiver3.ID, PhoneNumber: receiver3.PhoneNumber, Email: receiver3.Email, + ExternalID: receiver3.ExternalID, }, Wallet: Wallet{ ID: wallet.ID, @@ -1211,6 +1218,7 @@ func Test_ReceiverWallet_GetAllPendingRegistrationByDisbursementID(t *testing.T) ID: receiver4.ID, PhoneNumber: receiver4.PhoneNumber, Email: receiver4.Email, + ExternalID: receiver4.ExternalID, }, Wallet: Wallet{ ID: wallet.ID, diff --git a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go b/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go index 854e5801b..02931741b 100644 --- a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go +++ b/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go @@ -25,6 +25,7 @@ type SendReceiverWalletsSMSInvitationEventHandlerOptions struct { MaxInvitationSMSResendAttempts int64 Sep10SigningPrivateKey string CrashTrackerClient crashtracker.CrashTrackerClient + UseExternalID bool } type SendReceiverWalletsSMSInvitationEventHandler struct { @@ -49,6 +50,7 @@ func NewSendReceiverWalletsSMSInvitationEventHandler(options SendReceiverWallets options.Sep10SigningPrivateKey, options.MaxInvitationSMSResendAttempts, options.CrashTrackerClient, + options.UseExternalID, ) if err != nil { log.Fatalf("error instantiating service: %s", err.Error()) diff --git a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go index 1af631718..c82734d43 100644 --- a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go +++ b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go @@ -23,6 +23,7 @@ type SendReceiverWalletsSMSInvitationJobOptions struct { MaxInvitationSMSResendAttempts int64 Sep10SigningPrivateKey string CrashTrackerClient crashtracker.CrashTrackerClient + UseExternalID bool JobIntervalSeconds int } @@ -63,6 +64,7 @@ func NewSendReceiverWalletsSMSInvitationJob(options SendReceiverWalletsSMSInvita options.Sep10SigningPrivateKey, options.MaxInvitationSMSResendAttempts, options.CrashTrackerClient, + options.UseExternalID, ) if err != nil { log.Fatalf("error instantiating service: %s", err.Error()) diff --git a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go index 07b12be90..97fa90da8 100644 --- a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go +++ b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go @@ -142,6 +142,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { stellarSecretKey, maxInvitationSMSResendAttempts, crashTrackerClientMock, + true, ) require.NoError(t, err) @@ -205,6 +206,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { OrganizationName: "MyCustomAid", AssetCode: asset1.Code, AssetIssuer: asset1.Issuer, + ExternalID: receiver1.ExternalID, } deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) @@ -216,6 +218,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { OrganizationName: "MyCustomAid", AssetCode: asset2.Code, AssetIssuer: asset2.Issuer, + ExternalID: receiver2.ExternalID, } deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) diff --git a/internal/serve/httphandler/receiver_registration.go b/internal/serve/httphandler/receiver_registration.go index fbdd82178..a1d2cdd4a 100644 --- a/internal/serve/httphandler/receiver_registration.go +++ b/internal/serve/httphandler/receiver_registration.go @@ -17,6 +17,7 @@ type ReceiverRegistrationHandler struct { Models *data.Models ReceiverWalletModel *data.ReceiverWalletModel ReCAPTCHASiteKey string + UseExternalID bool } type ReceiverRegistrationData struct { diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler.go b/internal/serve/httphandler/verifiy_receiver_registration_handler.go index 88f45286a..b7a5ce2a5 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler.go @@ -114,6 +114,24 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( now := time.Now() truncatedPhoneNumber := utils.TruncateString(receiver.PhoneNumber, 3) + // STEP 1: if customer-id exists in sep24 claims, use it to look up receiver by externalID + // receiver's phone number will be compared to hashed mobile_number + if receiverRegistrationRequest.CustomerID != "" { + receiverByExternalID, err := v.Models.Receiver.GetByExternalID(ctx, dbTx, receiver.ExternalID) + if err != nil { + log.Ctx(ctx).Errorf("Error retrieving receiver by external ID: %v", err) + return &ErrorInformationNotFound{cause: err} + } + fmt.Println(receiverByExternalID.PhoneNumber) + + if data.CompareVerificationValue(receiverRegistrationRequest.MobileNumberHash, receiverByExternalID.PhoneNumber) { + return nil + } else { + err = fmt.Errorf("phone number validation failed for customer-id %s", receiverRegistrationRequest.CustomerID) + return &ErrorInformationNotFound{cause: err} + } + } + // STEP 1: find the receiverVerification entry that matches the pair [receiverID, verificationType] receiverVerifications, err := v.Models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{receiver.ID}, receiverRegistrationRequest.VerificationType) if err != nil { @@ -143,7 +161,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( receiverVerification.FailedAt = &now receiverVerification.ConfirmedAt = nil - // this update is done using the DBConnectionPool and not dbTx because we don't want to roolback these changes after returning the error + // this update is done using the DBConnectionPool and not dbTx because we don't want to rollback these changes after returning the error updateErr := v.Models.ReceiverVerification.UpdateReceiverVerification(ctx, *receiverVerification, v.Models.DBConnectionPool) if updateErr != nil { err = fmt.Errorf("%s: %w", baseErrMsg, updateErr) diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go index 45c4787a4..8f46fe4a2 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go @@ -124,6 +124,28 @@ func Test_VerifyReceiverRegistrationHandler_validate(t *testing.T) { ReCAPTCHAToken: "token", }, }, + { + name: "๐ŸŽ‰ successfully parses the body with external_id if the SEP24 token, recaptcha token and request body are all valid", + contextSep24Claims: sep24JWTClaims, + requestBody: `{ + "phone_number": "+380445555555", + "otp": "123456", + "verification": "1990-01-01", + "verification_type": "date_of_birth", + "external_id": "user-external-id", + "reCAPTCHA_token": "token" + }`, + isRecaptchaValidFnResponse: []interface{}{true, nil}, + wantSep24Claims: sep24JWTClaims, + wantResult: data.ReceiverRegistrationRequest{ + PhoneNumber: "+380445555555", + OTP: "123456", + VerificationValue: "1990-01-01", + VerificationType: data.VerificationFieldDateOfBirth, + ExternalID: "user-external-id", + ReCAPTCHAToken: "token", + }, + }, } models, err := data.NewModels(dbConnectionPool) @@ -320,6 +342,96 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te } } +func Test_VerifyReceiverRegistrationHandler_processReceiverCustomerID_and_MobileNumber(t *testing.T) { + ctx := context.Background() + dbt := dbtest.Open(t) + defer dbt.Close() + + // Open the connection pool and ensure it is closed at the end of the test. + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() // This defer should be right after checking the error. + + // Begin transaction and ensure it is either rolled back or committed. + dbTx, err := dbConnectionPool.BeginTxx(ctx, nil) + require.NoError(t, err) + defer func() { + require.NoError(t, dbTx.Rollback()) + }() + + models, err := data.NewModels(dbConnectionPool) + require.NoError(t, err) + + // Setting up mocks for services used in the handler. + mockAnchorPlatformService := anchorplatform.AnchorPlatformAPIServiceMock{} + reCAPTCHAValidator := &validators.ReCAPTCHAValidatorMock{} + handler := &VerifyReceiverRegistrationHandler{ + Models: models, + AnchorPlatformAPIService: &mockAnchorPlatformService, + ReCAPTCHAValidator: reCAPTCHAValidator, + } + + // Create a receiver fixture and ensure its cleanup. + receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+380443333333"}) + defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) // No need to defer twice as previously coded. + + // Hash the phone number and proceed to registration. + hashedReceiverPhoneNumber, err := data.HashVerificationValue(receiver.PhoneNumber) + if err != nil { + t.Fatal(err) + } + + // Registration request including a hash of the mobile number and a customer ID. + registrationRequest := data.ReceiverRegistrationRequest{ + MobileNumberHash: hashedReceiverPhoneNumber, + CustomerID: receiver.ExternalID, + } + + // Execute the method under test. + err = handler.processReceiverVerificationPII(ctx, dbTx, *receiver, registrationRequest) + require.NoError(t, err) +} + +func Test_VerifyReceiverRegistrationHandler_FailsWithInvalidMobileNumberHash(t *testing.T) { + ctx := context.Background() + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + dbTx, err := dbConnectionPool.BeginTxx(ctx, nil) + require.NoError(t, err) + defer func() { + require.NoError(t, dbTx.Rollback()) + }() + + models, err := data.NewModels(dbConnectionPool) + require.NoError(t, err) + + mockAnchorPlatformService := anchorplatform.AnchorPlatformAPIServiceMock{} + reCAPTCHAValidator := &validators.ReCAPTCHAValidatorMock{} + handler := &VerifyReceiverRegistrationHandler{ + Models: models, + AnchorPlatformAPIService: &mockAnchorPlatformService, + ReCAPTCHAValidator: reCAPTCHAValidator, + } + + receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+380443333333"}) + hashedReceiverPhoneNumber, err := data.HashVerificationValue("badmobilenumber") + if err != nil { + t.Fatal(err) + } + defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + + registrationRequest := data.ReceiverRegistrationRequest{ + MobileNumberHash: hashedReceiverPhoneNumber, + CustomerID: receiver.ExternalID, + } + + err = handler.processReceiverVerificationPII(ctx, dbTx, *receiver, registrationRequest) + require.Error(t, err) +} + func Test_VerifyReceiverRegistrationHandler_processReceiverWalletOTP(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -471,7 +583,7 @@ func Test_VerifyReceiverRegistrationHandler_processAnchorPlatformID(t *testing.T require.NoError(t, err) handler := &VerifyReceiverRegistrationHandler{Models: models} - // creeate fixtures + // create fixtures const phoneNumber = "+380445555555" defer data.DeleteAllFixtures(t, ctx, dbConnectionPool) wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://") diff --git a/internal/serve/middleware/middleware.go b/internal/serve/middleware/middleware.go index 1f7635487..dc9cf51c9 100644 --- a/internal/serve/middleware/middleware.go +++ b/internal/serve/middleware/middleware.go @@ -298,6 +298,7 @@ func ResolveTenantFromRequestMiddleware(tenantManager tenant.ManagerInterface, s if singleTenantMode { var err error currentTenant, err = tenantManager.GetDefault(ctx) + if err != nil { switch { case errors.Is(err, tenant.ErrTenantDoesNotExist): diff --git a/internal/serve/serve.go b/internal/serve/serve.go index e8ed7cc1a..d97907ed9 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -91,6 +91,7 @@ type ServeOptions struct { EventProducer events.Producer MaxInvitationSMSResendAttempts int SingleTenantMode bool + UseExternalID bool CircleService circle.ServiceInterface } @@ -175,7 +176,9 @@ func Serve(opts ServeOptions, httpServer HTTPServerInterface) error { ReadTimeout: time.Second * 5, WriteTimeout: time.Second * 35, IdleTimeout: time.Minute * 2, + OnStarting: func() { + log.Info("value of use-external-id is %t", opts.UseExternalID) log.Info("Starting SDP (Stellar Disbursement Platform) Server") log.Infof("Listening on %s", listenAddr) }, @@ -466,6 +469,7 @@ func handleHTTP(o ServeOptions) *chi.Mux { Models: o.Models, ReceiverWalletModel: o.Models.ReceiverWallet, ReCAPTCHASiteKey: o.ReCAPTCHASiteKey, + UseExternalID: o.UseExternalID, }.ServeHTTP) // This loads the SEP-24 PII registration webpage. sep24HeaderTokenAuthenticationMiddleware := anchorplatform.SEP24HeaderTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase, o.tenantManager, o.SingleTenantMode) diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 85988ad91..2c550e7a1 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -32,6 +32,7 @@ type SendReceiverWalletInviteService struct { maxInvitationSMSResendAttempts int64 sep10SigningPrivateKey string crashTrackerClient crashtracker.CrashTrackerClient + useExternalID bool } var _ SendReceiverWalletInviteServiceInterface = new(SendReceiverWalletInviteService) @@ -122,6 +123,11 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive TenantBaseURL: *currentTenant.BaseURL, } + // Only set ExternalID if useExternalID config is true + if s.useExternalID { + wdl.ExternalID = rwa.ReceiverWallet.Receiver.ExternalID + } + registrationLink, err := wdl.GetSignedRegistrationLink(s.sep10SigningPrivateKey) if err != nil { log.Ctx(ctx).Errorf( @@ -284,13 +290,14 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con return true } -func NewSendReceiverWalletInviteService(models *data.Models, messengerClient message.MessengerClient, sep10SigningPrivateKey string, maxInvitationSMSResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient) (*SendReceiverWalletInviteService, error) { +func NewSendReceiverWalletInviteService(models *data.Models, messengerClient message.MessengerClient, sep10SigningPrivateKey string, maxInvitationSMSResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient, useExternalID bool) (*SendReceiverWalletInviteService, error) { s := &SendReceiverWalletInviteService{ messengerClient: messengerClient, Models: models, maxInvitationSMSResendAttempts: maxInvitationSMSResendAttempts, sep10SigningPrivateKey: sep10SigningPrivateKey, crashTrackerClient: crashTrackerClient, + useExternalID: useExternalID, } if err := s.validate(); err != nil { @@ -311,6 +318,8 @@ type WalletDeepLink struct { AssetCode string // AssetIssuer is the issuer of the Stellar asset that the receiver will be able to receive. AssetIssuer string + // ExternalID is an optional parameter that can be used to include an external ID in the registration link. + ExternalID string // The external ID you want to include in the registration link // TenantBaseURL is the base URL for the tenant that the receiver wallet belongs to. TenantBaseURL string } @@ -441,6 +450,9 @@ func (wdl WalletDeepLink) GetUnsignedRegistrationLink() (string, error) { q.Add("domain", tomlFileDomain) q.Add("name", wdl.OrganizationName) q.Add("asset", wdl.assetName()) + if wdl.ExternalID != "" { + q.Add("external_id", wdl.ExternalID) + } u.RawQuery = q.Encode() diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index 9abbc4eb0..5d85051fc 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -29,11 +29,12 @@ func Test_GetSignedRegistrationLink_SchemelessDeepLink(t *testing.T) { AssetCode: "USDC", AssetIssuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", TenantBaseURL: "https://tenant.localhost.com", + ExternalID: "ExternalID", } registrationLink, err := wdl.GetSignedRegistrationLink("SCTOVDWM3A7KLTXXIV6YXL6QRVUIIG4HHHIDDKPR4JUB3DGDIKI5VGA2") require.NoError(t, err) - wantRegistrationLink := "https://api-dev.vibrantapp.com/sdp-dev?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=tenant.localhost.com&name=FOO+Org&signature=c6695a52ba8cc0ae2174023b116d4f726bc3d2c6d8d75a34336902ecbfa7eca07a059f44be503e3c4a71627aca66b05280b187e6614a0b130cf371328319ce0a" + wantRegistrationLink := "https://api-dev.vibrantapp.com/sdp-dev?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=tenant.localhost.com&external_id=ExternalID&name=FOO+Org&signature=e5bb177449321e418e1d3d74f4ef5d3c0dad27f9242d148b38bde497bc93abfb312c0f3c5fc87e89aa30f373d765af13cde84ac7a2fa5c86f36382a2de2f600f" require.Equal(t, wantRegistrationLink, registrationLink) wdl = WalletDeepLink{ @@ -42,11 +43,12 @@ func Test_GetSignedRegistrationLink_SchemelessDeepLink(t *testing.T) { OrganizationName: "FOO Org", AssetCode: "USDC", AssetIssuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + ExternalID: "ExternalID", } registrationLink, err = wdl.GetSignedRegistrationLink("SCTOVDWM3A7KLTXXIV6YXL6QRVUIIG4HHHIDDKPR4JUB3DGDIKI5VGA2") require.NoError(t, err) - wantRegistrationLink = "https://www.beansapp.com/disbursements/registration?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=tenant.localhost.com&name=FOO+Org&redirect=true&signature=ab27744802e712716cc2c282cb08cb327f1ed75c334152879dd2b2d880eb0c5cf250deb8ae11510e1d4db00ee1f8c15bf940760464ae27a4140ecdc32304780d" + wantRegistrationLink = "https://www.beansapp.com/disbursements/registration?asset=USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5&domain=tenant.localhost.com&external_id=ExternalID&name=FOO+Org&redirect=true&signature=4ef7096f37829e95eefd52c63ff4163570a3167e75fd7e253767b34700d50327c9fc4aaff03b7510a0201c08d67754b91b85fee5e443751c5d45bc30d418600c" require.Equal(t, wantRegistrationLink, registrationLink) } @@ -100,12 +102,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("returns error when service has wrong setup", func(t *testing.T) { - _, err := NewSendReceiverWalletInviteService(models, nil, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + _, err := NewSendReceiverWalletInviteService(models, nil, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) assert.EqualError(t, err, "invalid service setup: messenger client can't be nil") }) t.Run("inserts the failed sent message", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -245,7 +249,8 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("send invite successfully", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -375,8 +380,134 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Nil(t, msg.AssetID) }) + t.Run("send invite successfully with external_id", func(t *testing.T) { + useExternalID := true // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) + require.NoError(t, err) + + data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) + data.DeleteAllMessagesFixtures(t, ctx, dbConnectionPool) + data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) + + rec1RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.ReadyReceiversWalletStatus) + data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet2.ID, data.RegisteredReceiversWalletStatus) + + rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet2.ID, data.ReadyReceiversWalletStatus) + + _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + Status: data.ReadyPaymentStatus, + Disbursement: disbursement1, + Asset: *asset1, + ReceiverWallet: rec1RW, + Amount: "1", + }) + + _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + Status: data.ReadyPaymentStatus, + Disbursement: disbursement2, + Asset: *asset2, + ReceiverWallet: rec2RW, + Amount: "1", + }) + + walletDeepLink1 := WalletDeepLink{ + DeepLink: wallet1.DeepLinkSchema, + TenantBaseURL: "http://localhost:8000", + OrganizationName: "MyCustomAid", + AssetCode: asset1.Code, + AssetIssuer: asset1.Issuer, + ExternalID: rec1RW.Receiver.ExternalID, + } + deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) + require.NoError(t, err) + contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1) + + walletDeepLink2 := WalletDeepLink{ + DeepLink: wallet2.DeepLinkSchema, + TenantBaseURL: "http://localhost:8000", + OrganizationName: "MyCustomAid", + AssetCode: asset2.Code, + AssetIssuer: asset2.Issuer, + ExternalID: rec2RW.Receiver.ExternalID, + } + deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey) + require.NoError(t, err) + contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2) + + messengerClientMock. + On("SendMessage", message.Message{ + ToPhoneNumber: receiver1.PhoneNumber, + Message: contentWallet1, + }). + Return(nil). + Once(). + On("SendMessage", message.Message{ + ToPhoneNumber: receiver2.PhoneNumber, + Message: contentWallet2, + }). + Return(nil). + Once() + + err = s.SendInvite(ctx) + require.NoError(t, err) + + receivers, err := models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiver1.ID}, wallet1.ID) + require.NoError(t, err) + require.Len(t, receivers, 1) + assert.Equal(t, rec1RW.ID, receivers[0].ID) + assert.NotNil(t, receivers[0].InvitationSentAt) + + receivers, err = models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiver2.ID}, wallet2.ID) + require.NoError(t, err) + require.Len(t, receivers, 1) + assert.Equal(t, rec2RW.ID, receivers[0].ID) + assert.NotNil(t, receivers[0].InvitationSentAt) + + q := ` + SELECT + type, status, receiver_id, wallet_id, receiver_wallet_id, + title_encrypted, text_encrypted, status_history + FROM + messages + WHERE + receiver_id = $1 AND wallet_id = $2 AND receiver_wallet_id = $3 + ` + var msg data.Message + err = dbConnectionPool.GetContext(ctx, &msg, q, receiver1.ID, wallet1.ID, rec1RW.ID) + require.NoError(t, err) + + assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type) + assert.Equal(t, receiver1.ID, msg.ReceiverID) + assert.Equal(t, wallet1.ID, msg.WalletID) + assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) + assert.Equal(t, data.SuccessMessageStatus, msg.Status) + assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, contentWallet1, msg.TextEncrypted) + assert.Len(t, msg.StatusHistory, 2) + assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) + assert.Equal(t, data.SuccessMessageStatus, msg.StatusHistory[1].Status) + assert.Nil(t, msg.AssetID) + + msg = data.Message{} + err = dbConnectionPool.GetContext(ctx, &msg, q, receiver2.ID, wallet2.ID, rec2RW.ID) + require.NoError(t, err) + + assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type) + assert.Equal(t, receiver2.ID, msg.ReceiverID) + assert.Equal(t, wallet2.ID, msg.WalletID) + assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID) + assert.Equal(t, data.SuccessMessageStatus, msg.Status) + assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, contentWallet2, msg.TextEncrypted) + assert.Len(t, msg.StatusHistory, 2) + assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) + assert.Equal(t, data.SuccessMessageStatus, msg.StatusHistory[1].Status) + assert.Nil(t, msg.AssetID) + }) + t.Run("send invite successfully with custom invite message", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -511,7 +642,8 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend the invitation SMS when organization's SMS Resend Interval is nil and the invitation was already sent", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -556,7 +688,8 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend the invitation SMS when receiver reached the maximum number of resend attempts", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -636,7 +769,8 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend invitation SMS when receiver is not in the resend period", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -683,7 +817,8 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("successfully resend the invitation SMS", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + useExternalID := false // simulate default config value for useExternalID + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -775,6 +910,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("send disbursement invite successfully", func(t *testing.T) { + useExternalID := false // simulate default config value for useExternalID disbursement3 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ Country: country, Wallet: wallet1, @@ -791,7 +927,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement 4:", }) - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -922,6 +1058,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("successfully resend the disbursement invitation SMS", func(t *testing.T) { + useExternalID := false // simulate default config value for useExternalID disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ Country: country, Wallet: wallet1, @@ -930,7 +1067,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement:", }) - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient, useExternalID) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -1538,6 +1675,18 @@ func Test_WalletDeepLink_GetUnsignedRegistrationLink(t *testing.T) { }, wantResult: "wallet://sdp?asset=FOO-GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX&custom=true&domain=foo.bar&name=Foo+Bar+Org", }, + { + name: "๐ŸŽ‰ successful for deeplink with external-id", + walletDeepLink: WalletDeepLink{ + DeepLink: "wallet://sdp?custom=true", + TenantBaseURL: "foo.bar", + OrganizationName: "Foo Bar Org", + AssetCode: "FOO", + AssetIssuer: "GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX", + ExternalID: "123", + }, + wantResult: "wallet://sdp?asset=FOO-GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX&custom=true&domain=foo.bar&external_id=123&name=Foo+Bar+Org", + }, } for _, tc := range testCases {