diff --git a/.gitea/actions/build-proto/action.yml b/.gitea/actions/build-proto/action.yml new file mode 100644 index 0000000..a9e8bd9 --- /dev/null +++ b/.gitea/actions/build-proto/action.yml @@ -0,0 +1,33 @@ +name: Build Proto +description: Install buf CLI, run buf lint and buf build +inputs: + buf_version: + description: Buf CLI version to install + required: false + default: "1.72.0" +runs: + using: composite + steps: + - name: Setup Buf CLI + shell: bash + env: + BUF_VERSION: ${{ inputs.buf_version }} + run: | + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) BIN="buf-Linux-x86_64" ;; + aarch64) BIN="buf-Linux-aarch64" ;; + arm64) BIN="buf-Darwin-arm64" ;; + *) BIN="buf-Linux-x86_64" ;; + esac + curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/${BIN}.tar.gz" -o buf.tar.gz + tar -xzf buf.tar.gz + cp buf/bin/buf /usr/local/bin/buf + chmod +x /usr/local/bin/buf + rm -rf buf buf.tar.gz + - name: Lint + shell: bash + run: buf lint + - name: Build + shell: bash + run: buf build diff --git a/.gitea/actions/setup-mirrors/action.yml b/.gitea/actions/setup-mirrors/action.yml new file mode 100644 index 0000000..7d2d0bd --- /dev/null +++ b/.gitea/actions/setup-mirrors/action.yml @@ -0,0 +1,151 @@ +name: Setup package mirrors +description: Benchmark package mirrors and export the fastest reachable registry for each ecosystem +inputs: + npm_mirrors: + description: Newline-separated npm-compatible registries + required: false + default: | + https://npm.reg.darano.ir/ + pypi_mirrors: + required: false + default: | + https://package-mirror.liara.ir/repository/pypi/simple + https://pypi.reg.darano.ir + https://pypi-iran.ir/simple/ + https://pypi.org/simple/ + go_mirrors: + description: Newline-separated Go module proxies + required: false + default: | + https://go.devneeds.ir/ + https://package-mirror.liara.ir/repository/go/ + https://go.reg.darano.ir/ + https://proxy.golang.org/ + debian_mirrors: + description: Newline-separated Debian package repository base URLs + required: false + default: | + http://repo.iut.ac.ir/debian/ + http://deb.debian.org/debian/ + connect_timeout: + description: curl connection timeout in seconds + required: false + default: "3" + max_time: + description: curl request timeout in seconds + required: false + default: "10" +outputs: + npm_registry: + description: Fastest reachable npm registry + value: ${{ steps.select.outputs.npm_registry }} + pypi_index: + description: Fastest reachable Python package index + value: ${{ steps.select.outputs.pypi_index }} + go_proxy: + description: Fastest reachable Go module proxy + value: ${{ steps.select.outputs.go_proxy }} + debian_mirror: + description: Fastest reachable Debian package repository + value: ${{ steps.select.outputs.debian_mirror }} +runs: + using: composite + steps: + - name: Rate mirrors and export package environments + id: select + shell: bash + env: + NPM_MIRRORS: ${{ inputs.npm_mirrors }} + PYPI_MIRRORS: ${{ inputs.pypi_mirrors }} + GO_MIRRORS: ${{ inputs.go_mirrors }} + DEBIAN_MIRRORS: ${{ inputs.debian_mirrors }} + CONNECT_TIMEOUT: ${{ inputs.connect_timeout }} + MAX_TIME: ${{ inputs.max_time }} + run: | + set -euo pipefail + + if [[ ! "$CONNECT_TIMEOUT" =~ ^[0-9]+([.][0-9]+)?$ ]] || + [[ ! "$MAX_TIME" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + echo "Mirror timeouts must be positive numbers" >&2 + exit 1 + fi + + select_fastest() { + local ecosystem="$1" + local mirrors="$2" + local scores mirror result status duration fallback + scores="$(mktemp)" + fallback="" + + while IFS= read -r mirror; do + mirror="$(printf '%s' "$mirror" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [[ -z "$mirror" || "$mirror" == \#* ]] && continue + + if [[ ! "$mirror" =~ ^https?:// ]]; then + echo "[$ecosystem] skipped invalid URL: $mirror" >&2 + continue + fi + [[ -z "$fallback" ]] && fallback="$mirror" + + if result="$(curl \ + --location \ + --silent \ + --show-error \ + --output /dev/null \ + --connect-timeout "$CONNECT_TIMEOUT" \ + --max-time "$MAX_TIME" \ + --write-out '%{http_code} %{time_total}' \ + "$mirror" 2>/dev/null)"; then + read -r status duration <<< "$result" + if [[ "$status" =~ ^[0-9]{3}$ ]] && + (((10#$status >= 200 && 10#$status < 400) || 10#$status == 404)) && + [[ "$duration" =~ ^[0-9]+([.][0-9]+)?$ ]]; then + printf '%s\t%s\t%s\n' "$duration" "$status" "$mirror" >> "$scores" + continue + fi + fi + + echo "[$ecosystem] unavailable: $mirror" >&2 + done <<< "$mirrors" + + if [[ ! -s "$scores" ]]; then + rm -f "$scores" + if [[ -z "$fallback" ]]; then + echo "No valid $ecosystem mirror was configured" >&2 + return 1 + fi + echo "[$ecosystem] no mirror responded; falling back to $fallback" >&2 + printf '%s\n' "$fallback" + return 0 + fi + + echo "[$ecosystem] mirror ranking (seconds, HTTP status, URL):" >&2 + sort -n -k1,1 "$scores" >&2 + sort -n -k1,1 "$scores" | head -n 1 | cut -f3- + rm -f "$scores" + } + + npm_registry="$(select_fastest npm "$NPM_MIRRORS")" + pypi_index="$(select_fastest pypi "$PYPI_MIRRORS")" + go_proxy="$(select_fastest go "$GO_MIRRORS")" + debian_mirror="$(select_fastest debian "$DEBIAN_MIRRORS")" + + { + echo "NPM_CONFIG_REGISTRY=$npm_registry" + echo "npm_config_registry=$npm_registry" + echo "COREPACK_NPM_REGISTRY=$npm_registry" + echo "YARN_REGISTRY=$npm_registry" + echo "PNPM_CONFIG_REGISTRY=$npm_registry" + echo "BUN_CONFIG_REGISTRY=$npm_registry" + echo "PIP_INDEX_URL=$pypi_index" + echo "UV_DEFAULT_INDEX=$pypi_index" + echo "GOPROXY=$go_proxy" + echo "DEBIAN_MIRROR=$debian_mirror" + } >> "$GITHUB_ENV" + + { + echo "npm_registry=$npm_registry" + echo "pypi_index=$pypi_index" + echo "go_proxy=$go_proxy" + echo "debian_mirror=$debian_mirror" + } >> "$GITHUB_OUTPUT" diff --git a/.gitea/workflows/ci-stage.yaml b/.gitea/workflows/ci-stage.yaml new file mode 100644 index 0000000..db1d4c9 --- /dev/null +++ b/.gitea/workflows/ci-stage.yaml @@ -0,0 +1,52 @@ +--- +name: CI/CD Stage +on: + push: + branches: + - stage +jobs: + stage: + runs-on: ubuntu-latest + steps: + - name: Checkout Git + uses: https://git.darano.ir/actions/checkout@v5 + with: + token: ${{ gitea.token }} + path: ./ + submodules: recursive + - name: Checkout Buf CLI + uses: https://git.darano.ir/actions/checkout@v5 + with: + repository: actions/buf-bin + ref: main + path: buf-bin + - name: Setup package mirrors + uses: ./.gitea/actions/setup-mirrors + - name: Setup Buf CLI + run: | + BUF_VERSION=1.72.0 + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) BIN="buf-Linux-x86_64" ;; + aarch64) BIN="buf-Linux-aarch64" ;; + arm64) BIN="buf-Darwin-arm64" ;; + *) BIN="buf-Linux-x86_64" ;; + esac + curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/${BIN}.tar.gz" -o buf.tar.gz + tar -xzf buf.tar.gz + cp buf/bin/buf /usr/local/bin/buf + chmod +x /usr/local/bin/buf + rm -rf buf buf.tar.gz buf-bin + - name: Lint + run: buf lint + - name: Build + run: buf build + - name: Notify Telegram + if: always() + uses: ./.gitea/actions/notify-telegram + with: + bot_token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + chat_id: ${{ secrets.TELEGRAM_CHAT_ID }} + status: ${{ job.status }} + repository: ${{ gitea.repository }} + sha: ${{ gitea.sha }} diff --git a/alert/v1/srv.proto b/alert/v1/srv.proto index 0de2f2e..60520cd 100644 --- a/alert/v1/srv.proto +++ b/alert/v1/srv.proto @@ -7,8 +7,8 @@ import "base/v1/msg.proto"; service AlertSrv { rpc NotificationSrvHealth(base.v1.Empty) returns (base.v1.StatusRes); + rpc NotificationSMTPHealth(base.v1.Empty) returns (base.v1.StatusRes); rpc Emit(LogEvent) returns (base.v1.StatusRes) {} // rpc Update(LogEvent) returns (base.v1.StatusRes) {} // rpc Get(AlertFilter) returns (base.v1.StatusRes) {} } - diff --git a/auth/v1/internal_srv.proto b/auth/v1/internal_srv.proto index a06d2e5..3611892 100644 --- a/auth/v1/internal_srv.proto +++ b/auth/v1/internal_srv.proto @@ -6,6 +6,7 @@ import "auth/v1/msg.proto"; import "base/v1/msg.proto"; service InternalAuthorizationService { + rpc GetUserAnalytics(base.v1.Empty) returns (UserAnalytics); rpc InternalAuthorizationSrvHealth(base.v1.Empty) returns (base.v1.StatusRes); rpc LookUpName(LookUpNameReq) returns (LookUpNameRes) {} // This meant to only be used in a worker process @@ -15,4 +16,14 @@ service InternalAuthorizationService { rpc InitPermissionsForRoutes(InternalInitRoutesReq) returns (base.v1.StatusRes) {} rpc GetUserIdentityBasic(GetUserReq) returns (UserIdentityBasic) {} rpc GetUserIAM(GetUserReq) returns (InternalIAM) {} + rpc ListRoles(InternalRoleListReq) returns (RoleList) {} + rpc ListPermissions(InternalPermissionListReq) returns (PermissionList) {} + rpc CreateRoleAndAssign(CreateRoleAndAssignReq) returns (InternalIAM) {} + rpc AssignRoleToUser(AssignRoleToUserReq) returns (InternalIAM) {} + rpc AssignPermissionToUser(AssignPermissionToUserReq) returns (InternalIAM) {} +} + +message UserAnalytics { + int64 user_count = 1; + map user_count_by_kyc_level = 2; } diff --git a/auth/v1/msg.proto b/auth/v1/msg.proto index d7e4cf8..a8c9cb1 100644 --- a/auth/v1/msg.proto +++ b/auth/v1/msg.proto @@ -37,6 +37,44 @@ message PermissionList { uint32 total_count = 4; } +message RoleList { + repeated Role list = 1; + uint32 total_count = 2; +} + +message InternalRoleListReq { + string type = 1; +} + +message InternalPermissionListReq { + int64 role_id = 1; +} + +message CreateRoleAndAssignReq { + int64 user_id = 1; + string title = 2; + string key = 3; + string type = 4; + int64 parent_id = 5; + repeated int64 permission_ids = 6; +} + +message AssignRoleToUserReq { + int64 user_id = 1; + int64 role_id = 2; + bool assigned = 3; +} + +// Users only carry role IDs in the authorization schema. When role_id is +// omitted, the service creates a private role for this user and grants the +// requested permission through that role. +message AssignPermissionToUserReq { + int64 user_id = 1; + int64 permission_id = 2; + int64 role_id = 3; + bool assigned = 4; +} + /* UserSendOtp */ @@ -81,6 +119,7 @@ message CheckIAMReq { message InternalIAM { User user = 1; IdentityBasic identity = 2; + repeated string role_keys = 3; } message IdReqWithIAM { @@ -131,6 +170,7 @@ message User { UserStatus status = 5; repeated int64 roles = 6; int64 referrer_user_id = 7; + string referral_code = 8; } message UserAddress { diff --git a/auth/v1/srv.proto b/auth/v1/srv.proto index d90c2ea..2465a19 100644 --- a/auth/v1/srv.proto +++ b/auth/v1/srv.proto @@ -31,7 +31,9 @@ service AuthorizationService { rpc ProcessTFAReq(TFAReq) returns (base.v1.StatusRes) {} // For HMAC-OTP an initialization step must be added to exchange keys - rpc InitTFAReq(InternalIAM) returns (TFAExRes) {} + // Deprecated: authenticator-app enrollment was never implemented. Darano + // currently supports the ProcessTFAReq/CheckTFACode OTP flow. + rpc InitTFAReq(InternalIAM) returns (TFAExRes) { option deprecated = true; } rpc CheckTFACode(CheckTFAReq) returns (base.v1.StatusRes) {} } diff --git a/buf-Linux-x86_64.bin b/buf-Linux-x86_64.bin new file mode 100644 index 0000000..d65d13f Binary files /dev/null and b/buf-Linux-x86_64.bin differ diff --git a/buf.gen.yaml b/buf.gen.yaml index 4bc8897..c545e46 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -6,26 +6,26 @@ managed: override: - file_option: go_package_prefix # FIXME: Change this to you're go.mod package name - value: github.com/nfel + value: github.com/nfel plugins: - # NOTE: golang + # NOTE: golang # - remote: buf.build/grpc/go - # NOTE: grpc + # NOTE: grpc # go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - local: protoc-gen-go-grpc out: stub/go # NOTE: PB files # go install google.golang.org/protobuf/cmd/protoc-gen-go@latest - local: protoc-gen-go - # - remote: buf.build/protocolbuffers/go:v1.30.0 + # - remote: buf.build/protocolbuffers/go:v1.30.0 out: stub/go - # NOTE: doc - - local: protoc-gen-doc - opt: html,index.html,source_relative - out: stub/doc/html - - local: protoc-gen-doc - opt: markdown,index.md,source_relative - out: stub/doc/md + # # NOTE: doc + # - local: protoc-gen-doc + # opt: html,index.html,source_relative + # out: stub/doc/html + # - local: protoc-gen-doc + # opt: markdown,index.md,source_relative + # out: stub/doc/md # # NOTE: Gateway # # - remote: buf.build/grpc-ecosystem/gateway:v2.16.2 - local: protoc-gen-grpc-gateway @@ -36,27 +36,31 @@ plugins: # # NOTE: SWAGGER json files # # - remote: buf.build/grpc-ecosystem/openapiv2:v2.16.2 # - local: protoc-gen-openapiv2 - # opt: import_prefix=github.com/nfel + # opt: import_prefix=github.com/nfel # out: stub/go # - local: protoc-gen-go-gin - # opt: + # opt: # # - plugin=handler # # - plugin=service # # - plugin=mix # out: stub/go - # NOTE: Typescript + # NOTE: Typescript # - remote: buf.build/bufbuild/es:v2.2.2 - # # opt: import_prefix=github.com/nfel + # # opt: import_prefix=github.com/nfel # out: stub/ts - - local: protoc-gen-es - out: stub/ts - opt: target=ts - + # - local: protoc-gen-es + # out: stub/ts + # opt: target=ts + # # - local: protobuf-ts - # # opt: import_prefix=github.com/nfel + # # opt: import_prefix=github.com/nfel # out: stub/ts + - local: protoc-gen-prost + out: stub/rs + + - local: protoc-gen-tonic + out: stub/rs inputs: - directory: ./ # - git_repo: ssh://git@5.34.204.11/Kahroba/proto # branch: v2 - diff --git a/ledger/v1/msg.proto b/ledger/v1/msg.proto new file mode 100644 index 0000000..ab70fea --- /dev/null +++ b/ledger/v1/msg.proto @@ -0,0 +1,255 @@ +syntax = "proto3"; + +package ledger.v1; + +import "google/protobuf/timestamp.proto"; + +message OperatingModeResponse { + // NORMAL or GL_ONLY. No automatic outage failover. + string mode = 1; + int64 revision = 2; + string actor_id = 3; + string reason = 4; + google.protobuf.Timestamp changed_at = 5; +} + +message SetOperatingModeRequest { + string mode = 1; + int64 expected_revision = 2; + string actor_id = 3; + string reason = 4; + // Required when returning from GL_ONLY to NORMAL. + bool reconciliation_confirmed = 5; +} + +message GetUserBalancesRequest { + string owner_type = 1; + string owner_id = 2; + optional int64 asset_id = 3; +} + +message UserAssetBalance { + int64 asset_id = 1; + string available = 2; + string frozen = 3; + string total = 4; +} + +message GetUserBalancesResponse { + string owner_type = 1; + string owner_id = 2; + repeated UserAssetBalance balances = 3; +} + +enum AccountClass { + ACCOUNT_CLASS_UNSPECIFIED = 0; + ACCOUNT_CLASS_USER_AVAILABLE = 1; + ACCOUNT_CLASS_USER_FROZEN = 2; + ACCOUNT_CLASS_EXTERNAL_BLOCKCHAIN = 3; + ACCOUNT_CLASS_TREASURY = 4; + ACCOUNT_CLASS_MARKET_CLEARING = 5; + ACCOUNT_CLASS_IPG_CLEARING = 6; + ACCOUNT_CLASS_COMMISSION_REVENUE = 7; +} + +enum TransactionState { + TRANSACTION_STATE_UNSPECIFIED = 0; + TRANSACTION_STATE_CREATED = 1; + TRANSACTION_STATE_PENDING_TRANSACTION = 2; + TRANSACTION_STATE_PENDING_ADMIN = 3; + TRANSACTION_STATE_SUCCESSFUL = 4; + TRANSACTION_STATE_FAILED = 5; + TRANSACTION_STATE_SUSPENDED = 6; +} + +message AccountReference { + AccountClass account_class = 1; + string owner_type = 2; + string owner_id = 3; + int64 asset_id = 4; +} + +message JournalEntry { + uint32 line_number = 1; + AccountReference account = 2; + // Canonical base-10 decimal. Positive is credit and negative is debit. + string amount = 3; + string description = 4; +} + +message BlockchainReference { + string network = 1; + string transaction_hash = 2; + string ledger_sequence = 3; +} + +message Journal { + string journal_id = 1; + string source_service = 2; + string idempotency_key = 3; + string source_transaction_id = 4; + string tracking_code = 5; + string effect_kind = 6; + uint32 event_version = 7; + repeated JournalEntry entries = 8; + optional string reversal_of_journal_id = 9; + google.protobuf.Timestamp occurred_at = 10; + google.protobuf.Timestamp recorded_at = 11; + string correlation_id = 12; + string actor_id = 13; + BlockchainReference blockchain = 14; + map metadata = 15; + string payload_hash = 16; +} + +message AppendJournalRequest { + string source_service = 1; + string idempotency_key = 2; + string source_transaction_id = 3; + string tracking_code = 4; + string effect_kind = 5; + uint32 event_version = 6; + repeated JournalEntry entries = 7; + optional string reversal_of_journal_id = 8; + google.protobuf.Timestamp occurred_at = 9; + string correlation_id = 10; + string actor_id = 11; + BlockchainReference blockchain = 12; + map metadata = 13; +} + +message AppendJournalResponse { + Journal journal = 1; + bool already_existed = 2; +} + +message AppendTransactionEventRequest { + string source_service = 1; + string idempotency_key = 2; + string source_transaction_id = 3; + string tracking_code = 4; + uint32 event_version = 5; + TransactionState state = 6; + string error_code = 7; + string error_message = 8; + google.protobuf.Timestamp occurred_at = 9; + string correlation_id = 10; + string actor_id = 11; + BlockchainReference blockchain = 12; + map metadata = 13; +} + +message TransactionEvent { + string event_id = 1; + AppendTransactionEventRequest event = 2; + google.protobuf.Timestamp recorded_at = 3; + string payload_hash = 4; +} + +message AppendTransactionEventResponse { + TransactionEvent event = 1; + bool already_existed = 2; +} + +message GetJournalRequest { + oneof lookup { + string journal_id = 1; + string idempotency_key = 2; + } +} + +message ListEntriesRequest { + optional AccountReference account = 1; + optional int64 asset_id = 2; + google.protobuf.Timestamp recorded_from = 3; + google.protobuf.Timestamp recorded_to = 4; + uint32 page_size = 5; + string page_token = 6; +} + +message ListEntriesResponse { + repeated Journal journals = 1; + string next_page_token = 2; +} + +message GetBalanceRequest { + AccountReference account = 1; + google.protobuf.Timestamp as_of = 2; +} + +message GetBalanceResponse { + AccountReference account = 1; + // Canonical base-10 decimal reconstructed from immutable entries. + string balance = 2; + google.protobuf.Timestamp as_of = 3; +} + +message ReplayJournalsRequest { + // Each item is independently atomic and uses normal append idempotency. + repeated AppendJournalRequest journals = 1; +} + +message ReplayJournalResult { + string idempotency_key = 1; + Journal journal = 2; + bool already_existed = 3; +} + +message ReplayJournalsResponse { + repeated ReplayJournalResult results = 1; +} + +message HealthResponse { + bool serving = 1; + bool database_ready = 2; + bool settlement_enabled = 3; + bool settlement_ready = 4; + int64 settlement_pending = 5; + int64 settlement_retryable = 6; + int64 settlement_manual_review = 7; + int64 oldest_pending_seconds = 8; +} + +message EnqueueSettlementRequest { + string source_service = 1; + string source_transaction_id = 2; + string idempotency_key = 3; + string network = 4; + // Signed transaction XDR. GL persists and submits this payload durably. + string signed_transaction_xdr = 5; +} + +message EnqueueSettlementResponse { + string settlement_id = 1; + string status = 2; + bool already_existed = 3; +} + +message GetSettlementRequest { + oneof lookup { + string settlement_id = 1; + string idempotency_key = 2; + } +} + +message Settlement { + string settlement_id = 1; + string source_service = 2; + string source_transaction_id = 3; + string idempotency_key = 4; + string status = 5; + string network = 6; + string transaction_hash = 7; + uint32 attempts = 8; + string last_error = 9; +} + +message RetrySettlementRequest { + string settlement_id = 1; + string actor_id = 2; + string reason = 3; +} + +message RetrySettlementResponse { + Settlement settlement = 1; +} diff --git a/ledger/v1/srv.proto b/ledger/v1/srv.proto new file mode 100644 index 0000000..2043c16 --- /dev/null +++ b/ledger/v1/srv.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package ledger.v1; + +import "base/v1/msg.proto"; +import "ledger/v1/msg.proto"; + +service GeneralLedgerService { + rpc GetOperatingMode(base.v1.Empty) returns (OperatingModeResponse); + // Operator-only; requires x-admin-token and an optimistic revision. + rpc SetOperatingMode(SetOperatingModeRequest) returns (OperatingModeResponse); + // Trusted internal execution; requires x-admin-token and GL_ONLY mode. + rpc PostInternalJournal(AppendJournalRequest) returns (AppendJournalResponse); + rpc GetUserBalances(GetUserBalancesRequest) returns (GetUserBalancesResponse); + rpc Health(base.v1.Empty) returns (HealthResponse); + rpc AppendJournal(AppendJournalRequest) returns (AppendJournalResponse); + rpc AppendTransactionEvent(AppendTransactionEventRequest) returns (AppendTransactionEventResponse); + rpc GetJournal(GetJournalRequest) returns (Journal); + rpc ListEntries(ListEntriesRequest) returns (ListEntriesResponse); + rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse); + rpc ReplayJournals(ReplayJournalsRequest) returns (ReplayJournalsResponse); + rpc EnqueueSettlement(EnqueueSettlementRequest) returns (EnqueueSettlementResponse); + rpc GetSettlement(GetSettlementRequest) returns (Settlement); + // Restricted operator command. Requires the configured x-admin-token metadata. + rpc RetrySettlement(RetrySettlementRequest) returns (RetrySettlementResponse); +} diff --git a/market/v1/msg.proto b/market/v1/msg.proto index 9f21643..14a550a 100644 --- a/market/v1/msg.proto +++ b/market/v1/msg.proto @@ -71,6 +71,9 @@ message MarketOrder { repeated string tags = 20; repeated string comment = 21; optional MarketOrder source = 22; // purchase is done via this field + bool is_ico = 23; + optional string starts_at = 24; // RFC3339; absent means immediately available + optional string ends_at = 25; // RFC3339; absent means no scheduled end // repeated string attachments = 14; // possible files included by // seller/buyer } @@ -112,6 +115,15 @@ message MarketOrderList { uint32 total_count = 4; } +// AdminCancelOrderReq is used by trusted administrative tooling to cancel an +// order through the wallet service. The wallet service authenticates the +// caller with the administrative gRPC token; actor_user_id is retained for +// audit attribution. +message AdminCancelOrderReq { + uint64 order_id = 1; + int64 actor_user_id = 2; +} + /* * participant_type: IS Maker/Taker * side: IS Buying/Selling @@ -130,6 +142,9 @@ message NewMarketReq { MarketOrderSide side = 8; // TODO: maker market order as optional optional uint64 maker_order_id = 9; + bool is_ico = 10; + optional string starts_at = 11; // RFC3339 + optional string ends_at = 12; // RFC3339 } enum MarketOrdersSortBy { @@ -164,6 +179,7 @@ message OrderListFilter { optional MarketOrdersSortBy sort_by = 19; optional uint32 page_no = 21; optional uint32 page_size = 22; + optional bool is_ico = 23; } /* Contract */ diff --git a/market/v1/srv.proto b/market/v1/srv.proto index 5a1ef52..ee18073 100644 --- a/market/v1/srv.proto +++ b/market/v1/srv.proto @@ -5,6 +5,7 @@ package market.v1; import "auth/v1/msg.proto"; import "base/v1/msg.proto"; import "market/v1/msg.proto"; +import "wallet/v1/msg.proto"; service MarketplaceSrv { rpc MarketplaceSrvHealth(base.v1.Empty) returns (base.v1.StatusRes); @@ -16,10 +17,14 @@ service MarketplaceSrv { rpc GetMarketAssetList(MarketAssetListReq) returns (MarketAssetList); // assets that can be listed in marketplace rpc CalcMarketOrder(CalcMarketReq) returns (CalcMarketRes); + rpc CalcICOBuy(wallet.v1.BuyAssetReq) returns (wallet.v1.CalcBuyAssetRes); rpc CancelOrder(auth.v1.IdReqWithIAMAndTFA) returns (base.v1.StatusRes); // cancel an order + rpc AdminCancelOrder(AdminCancelOrderReq) returns (base.v1.StatusRes); // cancel an order from trusted admin tooling + rpc DeleteOrder(auth.v1.IdReqWithIAMAndTFA) returns (base.v1.StatusRes); // delete a closed order rpc NewMarketOrder(NewMarketReq) returns (MarketOrder); // insert new buy order to market rpc GenerateMarketContract(MarketContractReq) returns (ContractMarketRes); rpc ConfirmMarketContract(ConfirmMarketContractReq) returns (base.v1.StatusRes); + rpc SettleICOBuy(wallet.v1.ConfirmBuyAssetReq) returns (wallet.v1.BuyAssetRes); } diff --git a/wallet/v1/internal_srv.proto b/wallet/v1/internal_srv.proto index d890e42..b9e621a 100644 --- a/wallet/v1/internal_srv.proto +++ b/wallet/v1/internal_srv.proto @@ -15,9 +15,25 @@ service InternalWalletSrv { rpc CollectCommission(CommissionReq) returns (CommissionRes); rpc RefundCommission(RefundCommissionReq) returns (CommissionRes); - // Calculate stellar pub-priv key based on national id + // Deterministically derive the Stellar public key. This operation is local + // and has no database, Horizon, or GL side effects. rpc GetPublicKeyByNationalID(NationalIDReq) returns (PubKeyRes); - // Commission Logs + // Create and enqueue the on-chain account activation transaction. + rpc ActivateAccount(NationalIDReq) returns (base.v1.StatusRes); + + // Commission Logs rpc InitReferrerCommissionLog(InitReferrerCommissionLogIn) returns (base.v1.Empty); + + // Authenticated administrative asset lifecycle. Delete is represented as + // deactivation/locking so referenced financial history remains intact. + rpc AdminUpsertAsset(AdminUpsertAssetReq) returns (AdminAssetMutationRes); + rpc AdminDeactivateAsset(AdminDeactivateAssetReq) returns (base.v1.StatusRes); + + rpc AdminApproveIRTWithdrawal(AdminApproveIRTWithdrawalReq) returns (base.v1.StatusRes); + + // Bank statement receipts are checked and confirmed through the wallet owner. + // Confirmation is idempotent by bank_transaction_number. + rpc AdminCheckBankDeposit(AdminBankDepositReq) returns (AdminBankDepositRes); + rpc AdminConfirmBankDeposit(AdminBankDepositReq) returns (AdminBankDepositRes); } diff --git a/wallet/v1/msg.proto b/wallet/v1/msg.proto index 013ed93..8615518 100644 --- a/wallet/v1/msg.proto +++ b/wallet/v1/msg.proto @@ -9,31 +9,19 @@ import "base/v1/msg.proto"; INTERNAL DATA; */ +message Foo { + string first = 1; + oneof some_oneof { + string second = 2; + string third = 3; + } + string d = 4; +} + message InternalTransactionData { int64 transaction_id = 1; } -/* - Federation -*/ - -message Federation { - int64 id = 1; - int64 user_id = 2; - string ed_public = 3; - string ed_public_hash = 4; - int32 status = 5; - string created_at = 6; - string system_wallet_address = 7; -} - -message GetFederationReq { - optional int64 federation_id = 1; - optional int64 user_id = 2; - optional string ed_public = 3; - optional string ed_public_hash = 4; -} - /* Wallet */ @@ -59,15 +47,13 @@ message Wallet { int64 user_id = 2; int64 asset_id = 3; Asset asset_info = 4; - int64 federation_id = 5; - Federation federation_info = 6; - double balance = 7; - double frozen_balance = 8; - string updated_at = 9; - string created_at = 10; - string wallet_code = 11; - bool accepted_terms = 12; - bool is_locked = 13; + double balance = 5; + double frozen_balance = 6; + string updated_at = 7; + string created_at = 8; + string wallet_code = 9; + bool accepted_terms = 10; + bool is_locked = 11; } message BalanceReq { @@ -241,6 +227,33 @@ message LockAssetReq { optional auth.v1.TFA tfa = 4; // if request is not internal } +// Administrative approval is the only transition that may enqueue an IRT +// withdrawal for blockchain settlement. The receipt identifies the completed +// off-chain bank payment. +message AdminApproveIRTWithdrawalReq { + uint64 withdraw_log_id = 1; + string bank_receipt = 2; + int64 actor_user_id = 3; +} + +message AdminBankDepositReq { + string bank_transaction_number = 1; + string depositor_id = 2; // ten-digit national ID followed by the two-digit checksum + int64 amount_rial = 3; + string paid_at = 4; // RFC3339 + int64 user_id = 5; // wallet verifies this user's national ID against depositor_id + int64 actor_user_id = 6; +} + +message AdminBankDepositRes { + int64 user_id = 1; + string national_id = 2; + int64 transaction_id = 3; // zero until confirmed + bool already_registered = 4; + string amount_irt = 5; // exact decimal amount + string status = 6; +} + /* BNPL - not really but close name */ @@ -374,27 +387,22 @@ message Transaction { string asset_code = 3; optional int64 from_user_id = 4; optional auth.v1.UserIdentityBasic from_user_info = 5; - optional int64 from_federation_id = 6; - optional Federation from_federation_info = 7; - - optional int64 to_user_id = 8; - optional auth.v1.UserIdentityBasic to_user_info = 9; - optional int64 to_federation_id = 10; - optional Federation to_federation_info = 11; - double amount = 12; - TransactionStatus status = 13; - TransactionType type = 14; - string updated_at = 15; - string created_at = 16; - string tracking_code = 17; - optional string trx_hash = 18; - optional string from_public_key = 19; - optional string to_public_key = 20; - double token_price = 21; - BalanceChange balance_change = 22; + optional int64 to_user_id = 6; + optional auth.v1.UserIdentityBasic to_user_info = 7; + double amount = 8; + TransactionStatus status = 9; + TransactionType type = 10; + string updated_at = 11; + string created_at = 12; + string tracking_code = 13; + optional string trx_hash = 14; + optional string from_public_key = 15; + optional string to_public_key = 16; + double token_price = 17; + BalanceChange balance_change = 18; } -enum BalanceChange{ +enum BalanceChange { NO_CHANGE = 0; INCREASE = 1; DECREASE = 2; @@ -407,16 +415,14 @@ message TransactionListFilter { optional int64 id = 4; optional int64 asset_id = 5; optional int64 from_user_id = 6; - optional int64 from_federation_id = 7; - optional int64 to_user_id = 8; - optional int64 to_federation_id = 9; - optional double amount_from = 10; - optional double amount_to = 11; - optional string tracking_code = 12; - repeated TransactionType type = 13; - repeated TransactionStatus status = 14; - optional string from_date = 15; - optional string to_date = 16; + optional int64 to_user_id = 7; + optional double amount_from = 8; + optional double amount_to = 9; + optional string tracking_code = 10; + repeated TransactionType type = 11; + repeated TransactionStatus status = 12; + optional string from_date = 13; + optional string to_date = 14; } /* @@ -686,7 +692,6 @@ message IPGConfirmReq { // string amount = 7; } - message WithdrawLog { uint64 id = 1; Accounting accounting = 2; @@ -773,7 +778,7 @@ message WithdrawIRTReq { auth.v1.InternalIAM iam = 1; double amount = 2; int64 bank_info_id = 3; - auth.v1.TFA tfa = 4; + auth.v1.TFA tfa = 4; } message WithdrawIRTRes { @@ -814,12 +819,14 @@ message CalcBuyAssetRes { double calculated_asset_amount = 3; EffectiveCommission commission = 4; optional AssetDiscountRes discount_detail = 5; + uint64 maker_order_id = 6; } message BuyAssetRes { bool success = 1; string irt_hash = 2; string asset_hash = 3; + uint64 order_id = 4; } /* Contract */ @@ -840,3 +847,54 @@ message ContractRes { string contract_hash = 4; string contract_content = 5; } + +// Administrative asset commands are intentionally separate from the public +// Asset response. They contain the complete persistence-owned asset aggregate +// and are accepted only by the authenticated InternalWalletSrv boundary. +message AdminAssetInput { + int64 id = 1; + string name = 2; + string code = 3; + string issuer = 4; + string distributor = 5; + string network_code = 6; + string anchor = 7; + int32 decimal = 8; + string max_supply = 9; + optional string buy_min_amount = 10; + optional string buy_max_amount = 11; + string image = 12; + repeated string images = 13; + repeated string videos = 14; + string description = 15; + string url = 16; + string meta_json = 17; + bool is_active = 18; + bool is_locked = 19; + bool is_base_asset = 20; + bool show_if_empty = 21; + bool has_policy = 22; + bool has_whitelisting = 23; + bool can_buy = 24; + bool can_sell = 25; + bool can_deposit = 26; + bool can_withdraw = 27; + bool can_trade = 28; + AssetTokenType token_type = 29; + AssetType type = 30; + AssetStatus status = 31; +} + +message AdminUpsertAssetReq { + AdminAssetInput asset = 1; + uint64 actor_user_id = 2; +} + +message AdminDeactivateAssetReq { + int64 asset_id = 1; + uint64 actor_user_id = 2; +} + +message AdminAssetMutationRes { + int64 asset_id = 1; +} diff --git a/wallet/v1/srv.proto b/wallet/v1/srv.proto index 00475cc..bc8a3aa 100644 --- a/wallet/v1/srv.proto +++ b/wallet/v1/srv.proto @@ -7,6 +7,7 @@ import "base/v1/msg.proto"; import "wallet/v1/msg.proto"; service WalletService { + rpc GetWalletAnalytics(base.v1.Empty) returns (WalletAnalytics); rpc WalletSrvHealth(base.v1.Empty) returns (base.v1.StatusRes); rpc GetNetworkList(base.v1.Empty) returns (NetworkList); @@ -68,3 +69,9 @@ service WalletService { rpc DeclineBuyContract(DeclineBuyContractReq) returns (base.v1.StatusRes); // rpc GenerateMarketContract(MarketContractReq) returns (ContractRes); } + +message WalletAnalytics { + int64 transaction_count = 1; + map transaction_count_by_type = 2; + int64 account_count = 3; +}