diff --git a/Makefile b/Makefile index 445d0044f..01ccfe332 100755 --- a/Makefile +++ b/Makefile @@ -181,26 +181,15 @@ run-signer: @go run ./cmd/arkd-wallet ## run-simulation: run the multi-VTXO batch settlement test -## Usage: make run-simulation [CLIENTS=n] [MIN=n] [MAX=n] +## Usage: make run-simulation [CLIENTS=n] # Examples: -# make run-simulation # Default: 5 clients, min=5, max=128 -# make run-simulation CLIENTS=10 # 10 clients, min=10, max=128 -# make run-simulation CLIENTS=10 MAX=10 # 10 clients, exact batch size of 10 -# make run-simulation CLIENTS=20 MIN=5 # 20 clients, minimum batch size of 5 +# make run-simulation # Default: 5 clients +# make run-simulation CLIENTS=10 # 10 clients run-simulation: @echo "Stopping any existing Docker environment..." @docker compose -f docker-compose.regtest.yml down -v 2>/dev/null || true - @echo "Starting Docker environment with batch configuration..." - @bash -c '\ - CLIENTS="$${CLIENTS:-5}"; \ - MIN="$${MIN:-$$CLIENTS}"; \ - MAX="$${MAX:-128}"; \ - echo "Configuration: CLIENTS=$$CLIENTS, MIN=$$MIN, MAX=$$MAX"; \ - ARKD_ROUND_MIN_PARTICIPANTS_COUNT=$$MIN \ - ARKD_ROUND_MAX_PARTICIPANTS_COUNT=$$MAX \ - ARKD_SESSION_DURATION=60 \ - docker compose -f docker-compose.regtest.yml up --build -d; \ - ' + @echo "Starting Docker environment..." + @ARKD_SESSION_DURATION=60 docker compose -f docker-compose.regtest.yml up --build -d @echo "Waiting for services to start..." @sleep 30 @bash -c '\ diff --git a/README.md b/README.md index 7cab0227a..576afec7c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ In this documentation, you'll learn how to install and use `arkd`, a Bitcoin ser ### Configuration Options -The `arkd` server can be configured using environment variables. +The `arkd` server can be configured using environment variables and the admin settings API. + +#### Environment Variables | Environment Variable | Description | Default | |-------------------------------------|---------------------------------------------------------------------------------|--------------------------------| @@ -77,9 +79,6 @@ The `arkd` server can be configured using environment variables. | `ARKD_LIVE_STORE_TYPE` | Cache service type (redis, inmemory) | `redis` | | `ARKD_REDIS_URL` | Redis db connection url if `ARKD_LIVE_STORE_TYPE` is set to `redis` | - | | `ARKD_REDIS_NUM_OF_RETRIES` | Maximum number of retries for Redis write operations in case of conflicts | - | -| `ARKD_VTXO_TREE_EXPIRY` | VTXO tree expiry in seconds. Values below `512` are allowed only on regtest | `604672` (7 days) | -| `ARKD_UNILATERAL_EXIT_DELAY` | Unilateral exit delay in seconds | `86400` (24 hours) | -| `ARKD_BOARDING_EXIT_DELAY` | Boarding exit delay in seconds | `7776000` (3 months) | | `ARKD_ESPLORA_URL` | Esplora API URL | `https://blockstream.info/api` | | `ARKD_WALLET_ADDR` | The arkd wallet address to connect to in the form `host:port` | - | | `ARKD_SIGNER_ADDR` | The signer address to connect to in the form `host:port` | value of `ARKD_WALLET_ADDR` | @@ -88,15 +87,7 @@ The `arkd` server can be configured using environment variables. | `ARKD_UNLOCKER_TYPE` | Wallet unlocker type (env, file) to enable auto-unlock | - | | `ARKD_UNLOCKER_FILE_PATH` | Path to unlocker file | - | | `ARKD_UNLOCKER_PASSWORD` | Wallet unlocker password | - | -| `ARKD_ROUND_MAX_PARTICIPANTS_COUNT` | Maximum number of participants per round | `128` | -| `ARKD_ROUND_MIN_PARTICIPANTS_COUNT` | Minimum number of participants per round | `1` | -| `ARKD_UTXO_MAX_AMOUNT` | The maximum allowed amount for boarding or collaborative exit | `-1` (unset) | -| `ARKD_UTXO_MIN_AMOUNT` | The minimum allowed amount for boarding or collaborative exit | `-1` (dust) | -| `ARKD_VTXO_MAX_AMOUNT` | The maximum allowed amount for vtxos | `-1` (unset) | -| `ARKD_VTXO_MIN_AMOUNT` | The minimum allowed amount for vtxos | `-1` (dust) | -| `ARKD_BAN_DURATION` | Ban duration in seconds | `300` (5 minutes) | -| `ARKD_BAN_THRESHOLD` | Number of crimes to trigger a ban | `3` | -| `ARKD_CHECKPOINT_EXIT_DELAY` | Checkpoint exit delay in seconds | `86400` (24 hours) | +| `ARKD_SCHEDULER_TYPE` | Scheduler type (gocron, block) | `gocron` | | `ARKD_TLS_EXTRA_IP` | Extra IP addresses for TLS (comma-separated) | - | | `ARKD_TLS_EXTRA_DOMAIN` | Extra domains for TLS (comma-separated) | - | | `ARKD_NOTE_URI_PREFIX` | Note URI prefix | - | @@ -114,6 +105,35 @@ The `arkd` server can be configured using environment variables. | `ARKD_INDEXER_SIGNING_PRIVKEY` | Hex-encoded private key for indexer auth token signing (sensitive) | - | | `ARKD_INDEXER_AUTH_TOKEN_EXPIRY` | Auth token TTL in seconds | `300` (5 minutes) | +#### Admin Settings + +The following settings are persisted in the database and managed via the admin API. Default values are seeded on first startup. + +| Endpoint | Method | Description | +|-----------------------------------|--------|------------------------------------------| +| `/v1/admin/settings` | GET | Retrieve current settings | +| `/v1/admin/settings` | POST | Update settings (full replace) | +| `/v1/admin/settings/clear` | POST | Reset settings to defaults | + +| Setting | Description | Default | +|--------------------------------------|--------------------------------------------------------------|--------------------------------| +| `vtxo_tree_expiry` | VTXO tree expiry (blocks or seconds depending on scheduler) | `604672` (gocron) / `20` (block) | +| `unilateral_exit_delay` | Unilateral exit delay in seconds | `86400` (24 hours) | +| `public_unilateral_exit_delay` | Public unilateral exit delay in seconds | `86400` (24 hours) | +| `checkpoint_exit_delay` | Checkpoint exit delay (blocks or seconds) | `86400` (gocron) / `10` (block) | +| `boarding_exit_delay` | Boarding exit delay in seconds | `7776000` (3 months) | +| `round_min_participants_count` | Minimum number of participants per round | `1` | +| `round_max_participants_count` | Maximum number of participants per round | `128` | +| `vtxo_min_amount` | Minimum allowed amount for vtxos | `-1` (dust) | +| `vtxo_max_amount` | Maximum allowed amount for vtxos | `-1` (unset) | +| `utxo_min_amount` | Minimum allowed amount for boarding or collaborative exit | `-1` (dust) | +| `utxo_max_amount` | Maximum allowed amount for boarding or collaborative exit | `-1` (unset) | +| `ban_duration` | Ban duration in seconds | `300` (5 minutes) | +| `ban_threshold` | Number of crimes to trigger a ban | `3` | +| `max_tx_weight` | Max transaction weight | `40000` | +| `settlement_min_expiry_gap` | Minimum gap between settlement and VTXO expiry in seconds | `0` | +| `vtxo_no_csv_validation_cutoff_date` | Unix timestamp after which CSV validation is enforced | `0` (disabled) | + ## Provisioning ### Data Directory diff --git a/api-spec/openapi/swagger/ark/v1/admin.openapi.json b/api-spec/openapi/swagger/ark/v1/admin.openapi.json index a641d191a..b69a61dd0 100644 --- a/api-spec/openapi/swagger/ark/v1/admin.openapi.json +++ b/api-spec/openapi/swagger/ark/v1/admin.openapi.json @@ -833,6 +833,114 @@ } } }, + "/v1/admin/settings": { + "get": { + "tags": [ + "AdminService" + ], + "operationId": "AdminService_GetSettings", + "responses": { + "200": { + "description": "a successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSettingsResponse" + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + } + } + }, + "post": { + "tags": [ + "AdminService" + ], + "operationId": "AdminService_UpdateSettings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSettingsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "a successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSettingsResponse" + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + } + } + } + }, + "/v1/admin/settings/clear": { + "post": { + "tags": [ + "AdminService" + ], + "operationId": "AdminService_ClearSettings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearSettingsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "a successful response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearSettingsResponse" + } + } + } + }, + "default": { + "description": "An unexpected error response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Status" + } + } + } + } + } + } + }, "/v1/admin/sweep": { "post": { "tags": [ @@ -1033,6 +1141,14 @@ "title": "ClearScheduledSessionConfigResponse", "type": "object" }, + "ClearSettingsRequest": { + "title": "ClearSettingsRequest", + "type": "object" + }, + "ClearSettingsResponse": { + "title": "ClearSettingsResponse", + "type": "object" + }, "Conviction": { "title": "Conviction", "type": "object", @@ -1415,6 +1531,19 @@ } } }, + "GetSettingsRequest": { + "title": "GetSettingsRequest", + "type": "object" + }, + "GetSettingsResponse": { + "title": "GetSettingsResponse", + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Settings" + } + } + }, "Intent": { "title": "Intent", "type": "object", @@ -1707,6 +1836,80 @@ } } }, + "Settings": { + "title": "Settings", + "type": "object", + "properties": { + "banDuration": { + "type": "integer", + "format": "int64" + }, + "banThreshold": { + "type": "integer", + "format": "int64" + }, + "boardingExitDelay": { + "type": "integer", + "format": "int64" + }, + "checkpointExitDelay": { + "type": "integer", + "format": "int64" + }, + "maxTxWeight": { + "type": "integer", + "format": "int64" + }, + "publicUnilateralExitDelay": { + "type": "integer", + "format": "int64" + }, + "roundMaxParticipantsCount": { + "type": "integer", + "format": "int64" + }, + "roundMinParticipantsCount": { + "type": "integer", + "format": "int64" + }, + "settlementMinExpiryGap": { + "type": "integer", + "format": "int64" + }, + "unilateralExitDelay": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + }, + "utxoMaxAmount": { + "type": "integer", + "format": "int64" + }, + "utxoMinAmount": { + "type": "integer", + "format": "int64" + }, + "vtxoMaxAmount": { + "type": "integer", + "format": "int64" + }, + "vtxoMinAmount": { + "type": "integer", + "format": "int64" + }, + "vtxoNoCsvValidationCutoffDate": { + "type": "integer", + "format": "int64" + }, + "vtxoTreeExpiry": { + "type": "integer", + "format": "int64" + } + } + }, "Status": { "title": "Status", "type": "object", @@ -1819,6 +2022,26 @@ "UpdateScheduledSessionConfigResponse": { "title": "UpdateScheduledSessionConfigResponse", "type": "object" + }, + "UpdateSettingsRequest": { + "title": "UpdateSettingsRequest", + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/Settings" + }, + "updateFields": { + "type": "array", + "description": "Specifies which settings fields to update. If non-empty, only the\nlisted fields are written from the request; all other fields remain\nunchanged. If empty, every field in the settings message is written\nas-is. Fields not set in the request default to 0, so callers must\npopulate all fields. Use snake_case field names (e.g. \"ban_threshold\").", + "items": { + "type": "string" + } + } + } + }, + "UpdateSettingsResponse": { + "title": "UpdateSettingsResponse", + "type": "object" } } }, diff --git a/api-spec/protobuf/ark/v1/admin.proto b/api-spec/protobuf/ark/v1/admin.proto index ba0bbf4fc..1e2737150 100644 --- a/api-spec/protobuf/ark/v1/admin.proto +++ b/api-spec/protobuf/ark/v1/admin.proto @@ -138,6 +138,23 @@ service AdminService { body: "*" }; } + rpc GetSettings(GetSettingsRequest) returns (GetSettingsResponse) { + option (meshapi.gateway.http) = { + get: "/v1/admin/settings" + }; + } + rpc UpdateSettings(UpdateSettingsRequest) returns (UpdateSettingsResponse) { + option (meshapi.gateway.http) = { + post: "/v1/admin/settings" + body: "*" + }; + } + rpc ClearSettings(ClearSettingsRequest) returns (ClearSettingsResponse) { + option (meshapi.gateway.http) = { + post: "/v1/admin/settings/clear" + body: "*" + }; + } } message GetScheduledSweepRequest {} @@ -373,6 +390,45 @@ message SweepResponse { string hex = 2; } +message Settings { + int64 ban_threshold = 1; + int64 ban_duration = 2; + int64 unilateral_exit_delay = 3; + int64 public_unilateral_exit_delay = 4; + int64 checkpoint_exit_delay = 5; + int64 boarding_exit_delay = 6; + int64 vtxo_tree_expiry = 7; + int64 round_min_participants_count = 8; + int64 round_max_participants_count = 9; + int64 vtxo_min_amount = 10; + int64 vtxo_max_amount = 11; + int64 utxo_min_amount = 12; + int64 utxo_max_amount = 13; + int64 settlement_min_expiry_gap = 14; + int64 vtxo_no_csv_validation_cutoff_date = 15; + int64 max_tx_weight = 16; + int64 updated_at = 17; +} + +message GetSettingsRequest {} +message GetSettingsResponse { + Settings settings = 1; +} + +message UpdateSettingsRequest { + Settings settings = 1; + // Specifies which settings fields to update. If non-empty, only the + // listed fields are written from the request; all other fields remain + // unchanged. If empty, every field in the settings message is written + // as-is. Fields not set in the request default to 0, so callers must + // populate all fields. Use snake_case field names (e.g. "ban_threshold"). + repeated string update_fields = 2; +} +message UpdateSettingsResponse {} + +message ClearSettingsRequest {} +message ClearSettingsResponse {} + message ListTokensRequest { string token = 1; // base64 auth token string hash = 2; // hex-encoded hash of outpoints @@ -397,4 +453,4 @@ message RevokeTokensRequest { } message RevokeTokensResponse { int32 revoked_count = 1; -} \ No newline at end of file +} diff --git a/api-spec/protobuf/gen/ark/v1/admin.pb.go b/api-spec/protobuf/gen/ark/v1/admin.pb.go index bab2263b8..4eb7d2ebc 100644 --- a/api-spec/protobuf/gen/ark/v1/admin.pb.go +++ b/api-spec/protobuf/gen/ark/v1/admin.pb.go @@ -2762,6 +2762,423 @@ func (x *SweepResponse) GetHex() string { return "" } +type Settings struct { + state protoimpl.MessageState `protogen:"open.v1"` + BanThreshold int64 `protobuf:"varint,1,opt,name=ban_threshold,json=banThreshold,proto3" json:"ban_threshold,omitempty"` + BanDuration int64 `protobuf:"varint,2,opt,name=ban_duration,json=banDuration,proto3" json:"ban_duration,omitempty"` + UnilateralExitDelay int64 `protobuf:"varint,3,opt,name=unilateral_exit_delay,json=unilateralExitDelay,proto3" json:"unilateral_exit_delay,omitempty"` + PublicUnilateralExitDelay int64 `protobuf:"varint,4,opt,name=public_unilateral_exit_delay,json=publicUnilateralExitDelay,proto3" json:"public_unilateral_exit_delay,omitempty"` + CheckpointExitDelay int64 `protobuf:"varint,5,opt,name=checkpoint_exit_delay,json=checkpointExitDelay,proto3" json:"checkpoint_exit_delay,omitempty"` + BoardingExitDelay int64 `protobuf:"varint,6,opt,name=boarding_exit_delay,json=boardingExitDelay,proto3" json:"boarding_exit_delay,omitempty"` + VtxoTreeExpiry int64 `protobuf:"varint,7,opt,name=vtxo_tree_expiry,json=vtxoTreeExpiry,proto3" json:"vtxo_tree_expiry,omitempty"` + RoundMinParticipantsCount int64 `protobuf:"varint,8,opt,name=round_min_participants_count,json=roundMinParticipantsCount,proto3" json:"round_min_participants_count,omitempty"` + RoundMaxParticipantsCount int64 `protobuf:"varint,9,opt,name=round_max_participants_count,json=roundMaxParticipantsCount,proto3" json:"round_max_participants_count,omitempty"` + VtxoMinAmount int64 `protobuf:"varint,10,opt,name=vtxo_min_amount,json=vtxoMinAmount,proto3" json:"vtxo_min_amount,omitempty"` + VtxoMaxAmount int64 `protobuf:"varint,11,opt,name=vtxo_max_amount,json=vtxoMaxAmount,proto3" json:"vtxo_max_amount,omitempty"` + UtxoMinAmount int64 `protobuf:"varint,12,opt,name=utxo_min_amount,json=utxoMinAmount,proto3" json:"utxo_min_amount,omitempty"` + UtxoMaxAmount int64 `protobuf:"varint,13,opt,name=utxo_max_amount,json=utxoMaxAmount,proto3" json:"utxo_max_amount,omitempty"` + SettlementMinExpiryGap int64 `protobuf:"varint,14,opt,name=settlement_min_expiry_gap,json=settlementMinExpiryGap,proto3" json:"settlement_min_expiry_gap,omitempty"` + VtxoNoCsvValidationCutoffDate int64 `protobuf:"varint,15,opt,name=vtxo_no_csv_validation_cutoff_date,json=vtxoNoCsvValidationCutoffDate,proto3" json:"vtxo_no_csv_validation_cutoff_date,omitempty"` + MaxTxWeight int64 `protobuf:"varint,16,opt,name=max_tx_weight,json=maxTxWeight,proto3" json:"max_tx_weight,omitempty"` + UpdatedAt int64 `protobuf:"varint,17,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Settings) Reset() { + *x = Settings{} + mi := &file_ark_v1_admin_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Settings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Settings) ProtoMessage() {} + +func (x *Settings) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Settings.ProtoReflect.Descriptor instead. +func (*Settings) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{52} +} + +func (x *Settings) GetBanThreshold() int64 { + if x != nil { + return x.BanThreshold + } + return 0 +} + +func (x *Settings) GetBanDuration() int64 { + if x != nil { + return x.BanDuration + } + return 0 +} + +func (x *Settings) GetUnilateralExitDelay() int64 { + if x != nil { + return x.UnilateralExitDelay + } + return 0 +} + +func (x *Settings) GetPublicUnilateralExitDelay() int64 { + if x != nil { + return x.PublicUnilateralExitDelay + } + return 0 +} + +func (x *Settings) GetCheckpointExitDelay() int64 { + if x != nil { + return x.CheckpointExitDelay + } + return 0 +} + +func (x *Settings) GetBoardingExitDelay() int64 { + if x != nil { + return x.BoardingExitDelay + } + return 0 +} + +func (x *Settings) GetVtxoTreeExpiry() int64 { + if x != nil { + return x.VtxoTreeExpiry + } + return 0 +} + +func (x *Settings) GetRoundMinParticipantsCount() int64 { + if x != nil { + return x.RoundMinParticipantsCount + } + return 0 +} + +func (x *Settings) GetRoundMaxParticipantsCount() int64 { + if x != nil { + return x.RoundMaxParticipantsCount + } + return 0 +} + +func (x *Settings) GetVtxoMinAmount() int64 { + if x != nil { + return x.VtxoMinAmount + } + return 0 +} + +func (x *Settings) GetVtxoMaxAmount() int64 { + if x != nil { + return x.VtxoMaxAmount + } + return 0 +} + +func (x *Settings) GetUtxoMinAmount() int64 { + if x != nil { + return x.UtxoMinAmount + } + return 0 +} + +func (x *Settings) GetUtxoMaxAmount() int64 { + if x != nil { + return x.UtxoMaxAmount + } + return 0 +} + +func (x *Settings) GetSettlementMinExpiryGap() int64 { + if x != nil { + return x.SettlementMinExpiryGap + } + return 0 +} + +func (x *Settings) GetVtxoNoCsvValidationCutoffDate() int64 { + if x != nil { + return x.VtxoNoCsvValidationCutoffDate + } + return 0 +} + +func (x *Settings) GetMaxTxWeight() int64 { + if x != nil { + return x.MaxTxWeight + } + return 0 +} + +func (x *Settings) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + +type GetSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSettingsRequest) Reset() { + *x = GetSettingsRequest{} + mi := &file_ark_v1_admin_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSettingsRequest) ProtoMessage() {} + +func (x *GetSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSettingsRequest.ProtoReflect.Descriptor instead. +func (*GetSettingsRequest) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{53} +} + +type GetSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Settings *Settings `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSettingsResponse) Reset() { + *x = GetSettingsResponse{} + mi := &file_ark_v1_admin_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSettingsResponse) ProtoMessage() {} + +func (x *GetSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetSettingsResponse.ProtoReflect.Descriptor instead. +func (*GetSettingsResponse) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{54} +} + +func (x *GetSettingsResponse) GetSettings() *Settings { + if x != nil { + return x.Settings + } + return nil +} + +type UpdateSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Settings *Settings `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` + // Specifies which settings fields to update. If non-empty, only the + // listed fields are written from the request; all other fields remain + // unchanged. If empty, every field in the settings message is written + // as-is. Fields not set in the request default to 0, so callers must + // populate all fields. Use snake_case field names (e.g. "ban_threshold"). + UpdateFields []string `protobuf:"bytes,2,rep,name=update_fields,json=updateFields,proto3" json:"update_fields,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSettingsRequest) Reset() { + *x = UpdateSettingsRequest{} + mi := &file_ark_v1_admin_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSettingsRequest) ProtoMessage() {} + +func (x *UpdateSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateSettingsRequest.ProtoReflect.Descriptor instead. +func (*UpdateSettingsRequest) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{55} +} + +func (x *UpdateSettingsRequest) GetSettings() *Settings { + if x != nil { + return x.Settings + } + return nil +} + +func (x *UpdateSettingsRequest) GetUpdateFields() []string { + if x != nil { + return x.UpdateFields + } + return nil +} + +type UpdateSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateSettingsResponse) Reset() { + *x = UpdateSettingsResponse{} + mi := &file_ark_v1_admin_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateSettingsResponse) ProtoMessage() {} + +func (x *UpdateSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateSettingsResponse.ProtoReflect.Descriptor instead. +func (*UpdateSettingsResponse) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{56} +} + +type ClearSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearSettingsRequest) Reset() { + *x = ClearSettingsRequest{} + mi := &file_ark_v1_admin_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearSettingsRequest) ProtoMessage() {} + +func (x *ClearSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearSettingsRequest.ProtoReflect.Descriptor instead. +func (*ClearSettingsRequest) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{57} +} + +type ClearSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClearSettingsResponse) Reset() { + *x = ClearSettingsResponse{} + mi := &file_ark_v1_admin_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClearSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClearSettingsResponse) ProtoMessage() {} + +func (x *ClearSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_ark_v1_admin_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClearSettingsResponse.ProtoReflect.Descriptor instead. +func (*ClearSettingsResponse) Descriptor() ([]byte, []int) { + return file_ark_v1_admin_proto_rawDescGZIP(), []int{58} +} + type ListTokensRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // base64 auth token @@ -2774,7 +3191,7 @@ type ListTokensRequest struct { func (x *ListTokensRequest) Reset() { *x = ListTokensRequest{} - mi := &file_ark_v1_admin_proto_msgTypes[52] + mi := &file_ark_v1_admin_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2786,7 +3203,7 @@ func (x *ListTokensRequest) String() string { func (*ListTokensRequest) ProtoMessage() {} func (x *ListTokensRequest) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_admin_proto_msgTypes[52] + mi := &file_ark_v1_admin_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2799,7 +3216,7 @@ func (x *ListTokensRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTokensRequest.ProtoReflect.Descriptor instead. func (*ListTokensRequest) Descriptor() ([]byte, []int) { - return file_ark_v1_admin_proto_rawDescGZIP(), []int{52} + return file_ark_v1_admin_proto_rawDescGZIP(), []int{59} } func (x *ListTokensRequest) GetToken() string { @@ -2839,7 +3256,7 @@ type ListTokensResponse struct { func (x *ListTokensResponse) Reset() { *x = ListTokensResponse{} - mi := &file_ark_v1_admin_proto_msgTypes[53] + mi := &file_ark_v1_admin_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2851,7 +3268,7 @@ func (x *ListTokensResponse) String() string { func (*ListTokensResponse) ProtoMessage() {} func (x *ListTokensResponse) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_admin_proto_msgTypes[53] + mi := &file_ark_v1_admin_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2864,7 +3281,7 @@ func (x *ListTokensResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTokensResponse.ProtoReflect.Descriptor instead. func (*ListTokensResponse) Descriptor() ([]byte, []int) { - return file_ark_v1_admin_proto_rawDescGZIP(), []int{53} + return file_ark_v1_admin_proto_rawDescGZIP(), []int{60} } func (x *ListTokensResponse) GetTokens() []*TokenInfo { @@ -2885,7 +3302,7 @@ type TokenInfo struct { func (x *TokenInfo) Reset() { *x = TokenInfo{} - mi := &file_ark_v1_admin_proto_msgTypes[54] + mi := &file_ark_v1_admin_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2897,7 +3314,7 @@ func (x *TokenInfo) String() string { func (*TokenInfo) ProtoMessage() {} func (x *TokenInfo) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_admin_proto_msgTypes[54] + mi := &file_ark_v1_admin_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2910,7 +3327,7 @@ func (x *TokenInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenInfo.ProtoReflect.Descriptor instead. func (*TokenInfo) Descriptor() ([]byte, []int) { - return file_ark_v1_admin_proto_rawDescGZIP(), []int{54} + return file_ark_v1_admin_proto_rawDescGZIP(), []int{61} } func (x *TokenInfo) GetHash() string { @@ -2946,7 +3363,7 @@ type RevokeTokensRequest struct { func (x *RevokeTokensRequest) Reset() { *x = RevokeTokensRequest{} - mi := &file_ark_v1_admin_proto_msgTypes[55] + mi := &file_ark_v1_admin_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2958,7 +3375,7 @@ func (x *RevokeTokensRequest) String() string { func (*RevokeTokensRequest) ProtoMessage() {} func (x *RevokeTokensRequest) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_admin_proto_msgTypes[55] + mi := &file_ark_v1_admin_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2971,7 +3388,7 @@ func (x *RevokeTokensRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeTokensRequest.ProtoReflect.Descriptor instead. func (*RevokeTokensRequest) Descriptor() ([]byte, []int) { - return file_ark_v1_admin_proto_rawDescGZIP(), []int{55} + return file_ark_v1_admin_proto_rawDescGZIP(), []int{62} } func (x *RevokeTokensRequest) GetToken() string { @@ -3011,7 +3428,7 @@ type RevokeTokensResponse struct { func (x *RevokeTokensResponse) Reset() { *x = RevokeTokensResponse{} - mi := &file_ark_v1_admin_proto_msgTypes[56] + mi := &file_ark_v1_admin_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3023,7 +3440,7 @@ func (x *RevokeTokensResponse) String() string { func (*RevokeTokensResponse) ProtoMessage() {} func (x *RevokeTokensResponse) ProtoReflect() protoreflect.Message { - mi := &file_ark_v1_admin_proto_msgTypes[56] + mi := &file_ark_v1_admin_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3036,7 +3453,7 @@ func (x *RevokeTokensResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeTokensResponse.ProtoReflect.Descriptor instead. func (*RevokeTokensResponse) Descriptor() ([]byte, []int) { - return file_ark_v1_admin_proto_rawDescGZIP(), []int{56} + return file_ark_v1_admin_proto_rawDescGZIP(), []int{63} } func (x *RevokeTokensResponse) GetRevokedCount() int32 { @@ -3210,7 +3627,36 @@ const file_ark_v1_admin_proto_rawDesc = "" + "\x10commitment_txids\x18\x02 \x03(\tR\x0fcommitmentTxids\"5\n" + "\rSweepResponse\x12\x12\n" + "\x04txid\x18\x01 \x01(\tR\x04txid\x12\x10\n" + - "\x03hex\x18\x02 \x01(\tR\x03hex\"m\n" + + "\x03hex\x18\x02 \x01(\tR\x03hex\"\xc0\x06\n" + + "\bSettings\x12#\n" + + "\rban_threshold\x18\x01 \x01(\x03R\fbanThreshold\x12!\n" + + "\fban_duration\x18\x02 \x01(\x03R\vbanDuration\x122\n" + + "\x15unilateral_exit_delay\x18\x03 \x01(\x03R\x13unilateralExitDelay\x12?\n" + + "\x1cpublic_unilateral_exit_delay\x18\x04 \x01(\x03R\x19publicUnilateralExitDelay\x122\n" + + "\x15checkpoint_exit_delay\x18\x05 \x01(\x03R\x13checkpointExitDelay\x12.\n" + + "\x13boarding_exit_delay\x18\x06 \x01(\x03R\x11boardingExitDelay\x12(\n" + + "\x10vtxo_tree_expiry\x18\a \x01(\x03R\x0evtxoTreeExpiry\x12?\n" + + "\x1cround_min_participants_count\x18\b \x01(\x03R\x19roundMinParticipantsCount\x12?\n" + + "\x1cround_max_participants_count\x18\t \x01(\x03R\x19roundMaxParticipantsCount\x12&\n" + + "\x0fvtxo_min_amount\x18\n" + + " \x01(\x03R\rvtxoMinAmount\x12&\n" + + "\x0fvtxo_max_amount\x18\v \x01(\x03R\rvtxoMaxAmount\x12&\n" + + "\x0futxo_min_amount\x18\f \x01(\x03R\rutxoMinAmount\x12&\n" + + "\x0futxo_max_amount\x18\r \x01(\x03R\rutxoMaxAmount\x129\n" + + "\x19settlement_min_expiry_gap\x18\x0e \x01(\x03R\x16settlementMinExpiryGap\x12I\n" + + "\"vtxo_no_csv_validation_cutoff_date\x18\x0f \x01(\x03R\x1dvtxoNoCsvValidationCutoffDate\x12\"\n" + + "\rmax_tx_weight\x18\x10 \x01(\x03R\vmaxTxWeight\x12\x1d\n" + + "\n" + + "updated_at\x18\x11 \x01(\x03R\tupdatedAt\"\x14\n" + + "\x12GetSettingsRequest\"C\n" + + "\x13GetSettingsResponse\x12,\n" + + "\bsettings\x18\x01 \x01(\v2\x10.ark.v1.SettingsR\bsettings\"j\n" + + "\x15UpdateSettingsRequest\x12,\n" + + "\bsettings\x18\x01 \x01(\v2\x10.ark.v1.SettingsR\bsettings\x12#\n" + + "\rupdate_fields\x18\x02 \x03(\tR\fupdateFields\"\x18\n" + + "\x16UpdateSettingsResponse\"\x16\n" + + "\x14ClearSettingsRequest\"\x17\n" + + "\x15ClearSettingsResponse\"m\n" + "\x11ListTokensRequest\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12\x12\n" + "\x04hash\x18\x02 \x01(\tR\x04hash\x12\x1a\n" + @@ -3241,7 +3687,7 @@ const file_ark_v1_admin_proto_rawDesc = "" + "\x15CRIME_TYPE_MANUAL_BAN\x10\a*M\n" + "\x0eConvictionType\x12\x1f\n" + "\x1bCONVICTION_TYPE_UNSPECIFIED\x10\x00\x12\x1a\n" + - "\x16CONVICTION_TYPE_SCRIPT\x10\x012\xdb\x16\n" + + "\x16CONVICTION_TYPE_SCRIPT\x10\x012\x99\x19\n" + "\fAdminService\x12o\n" + "\x11GetScheduledSweep\x12 .ark.v1.GetScheduledSweepRequest\x1a!.ark.v1.GetScheduledSweepResponse\"\x15\xb2J\x12\x12\x10/v1/admin/sweeps\x12s\n" + "\x0fGetRoundDetails\x12\x1e.ark.v1.GetRoundDetailsRequest\x1a\x1f.ark.v1.GetRoundDetailsResponse\"\x1f\xb2J\x1c\x12\x1a/v1/admin/round/{round_id}\x12W\n" + @@ -3269,7 +3715,10 @@ const file_ark_v1_admin_proto_rawDesc = "" + "\fRevokeTokens\x12\x1b.ark.v1.RevokeTokensRequest\x1a\x1c.ark.v1.RevokeTokensResponse\"\x1f\xb2J\x1cB\x01*\"\x17/v1/admin/tokens/revoke\x12\x84\x01\n" + "\x14GetExpiringLiquidity\x12#.ark.v1.GetExpiringLiquidityRequest\x1a$.ark.v1.GetExpiringLiquidityResponse\"!\xb2J\x1e\x12\x1c/v1/admin/liquidity/expiring\x12\x90\x01\n" + "\x17GetRecoverableLiquidity\x12&.ark.v1.GetRecoverableLiquidityRequest\x1a'.ark.v1.GetRecoverableLiquidityResponse\"$\xb2J!\x12\x1f/v1/admin/liquidity/recoverable\x12M\n" + - "\x05Sweep\x12\x14.ark.v1.SweepRequest\x1a\x15.ark.v1.SweepResponse\"\x17\xb2J\x14B\x01*\"\x0f/v1/admin/sweepBy\n" + + "\x05Sweep\x12\x14.ark.v1.SweepRequest\x1a\x15.ark.v1.SweepResponse\"\x17\xb2J\x14B\x01*\"\x0f/v1/admin/sweep\x12_\n" + + "\vGetSettings\x12\x1a.ark.v1.GetSettingsRequest\x1a\x1b.ark.v1.GetSettingsResponse\"\x17\xb2J\x14\x12\x12/v1/admin/settings\x12k\n" + + "\x0eUpdateSettings\x12\x1d.ark.v1.UpdateSettingsRequest\x1a\x1e.ark.v1.UpdateSettingsResponse\"\x1a\xb2J\x17B\x01*\"\x12/v1/admin/settings\x12n\n" + + "\rClearSettings\x12\x1c.ark.v1.ClearSettingsRequest\x1a\x1d.ark.v1.ClearSettingsResponse\" \xb2J\x1dB\x01*\"\x18/v1/admin/settings/clearBy\n" + "\n" + "com.ark.v1B\n" + "AdminProtoP\x01Z&github.com/arkade-os/arkd/ark/v1;arkv1\xa2\x02\x03AXX\xaa\x02\x06Ark.V1\xca\x02\x06Ark\\V1\xe2\x02\x12Ark\\V1\\GPBMetadata\xea\x02\aArk::V1b\x06proto3" @@ -3287,7 +3736,7 @@ func file_ark_v1_admin_proto_rawDescGZIP() []byte { } var file_ark_v1_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_ark_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 57) +var file_ark_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 64) var file_ark_v1_admin_proto_goTypes = []any{ (CrimeType)(0), // 0: ark.v1.CrimeType (ConvictionType)(0), // 1: ark.v1.ConvictionType @@ -3343,13 +3792,20 @@ var file_ark_v1_admin_proto_goTypes = []any{ (*GetRecoverableLiquidityResponse)(nil), // 51: ark.v1.GetRecoverableLiquidityResponse (*SweepRequest)(nil), // 52: ark.v1.SweepRequest (*SweepResponse)(nil), // 53: ark.v1.SweepResponse - (*ListTokensRequest)(nil), // 54: ark.v1.ListTokensRequest - (*ListTokensResponse)(nil), // 55: ark.v1.ListTokensResponse - (*TokenInfo)(nil), // 56: ark.v1.TokenInfo - (*RevokeTokensRequest)(nil), // 57: ark.v1.RevokeTokensRequest - (*RevokeTokensResponse)(nil), // 58: ark.v1.RevokeTokensResponse - (*FeeInfo)(nil), // 59: ark.v1.FeeInfo - (*Intent)(nil), // 60: ark.v1.Intent + (*Settings)(nil), // 54: ark.v1.Settings + (*GetSettingsRequest)(nil), // 55: ark.v1.GetSettingsRequest + (*GetSettingsResponse)(nil), // 56: ark.v1.GetSettingsResponse + (*UpdateSettingsRequest)(nil), // 57: ark.v1.UpdateSettingsRequest + (*UpdateSettingsResponse)(nil), // 58: ark.v1.UpdateSettingsResponse + (*ClearSettingsRequest)(nil), // 59: ark.v1.ClearSettingsRequest + (*ClearSettingsResponse)(nil), // 60: ark.v1.ClearSettingsResponse + (*ListTokensRequest)(nil), // 61: ark.v1.ListTokensRequest + (*ListTokensResponse)(nil), // 62: ark.v1.ListTokensResponse + (*TokenInfo)(nil), // 63: ark.v1.TokenInfo + (*RevokeTokensRequest)(nil), // 64: ark.v1.RevokeTokensRequest + (*RevokeTokensResponse)(nil), // 65: ark.v1.RevokeTokensResponse + (*FeeInfo)(nil), // 66: ark.v1.FeeInfo + (*Intent)(nil), // 67: ark.v1.Intent } var file_ark_v1_admin_proto_depIdxs = []int32{ 41, // 0: ark.v1.GetScheduledSweepResponse.sweeps:type_name -> ark.v1.ScheduledSweep @@ -3363,67 +3819,75 @@ var file_ark_v1_admin_proto_depIdxs = []int32{ 47, // 8: ark.v1.GetConvictionsByRoundResponse.convictions:type_name -> ark.v1.Conviction 47, // 9: ark.v1.GetActiveScriptConvictionsResponse.convictions:type_name -> ark.v1.Conviction 40, // 10: ark.v1.ScheduledSweep.outputs:type_name -> ark.v1.SweepableOutput - 59, // 11: ark.v1.ScheduledSessionConfig.fees:type_name -> ark.v1.FeeInfo + 66, // 11: ark.v1.ScheduledSessionConfig.fees:type_name -> ark.v1.FeeInfo 46, // 12: ark.v1.IntentInfo.receivers:type_name -> ark.v1.Output 43, // 13: ark.v1.IntentInfo.inputs:type_name -> ark.v1.IntentInput 43, // 14: ark.v1.IntentInfo.boarding_inputs:type_name -> ark.v1.IntentInput - 60, // 15: ark.v1.IntentInfo.intent:type_name -> ark.v1.Intent + 67, // 15: ark.v1.IntentInfo.intent:type_name -> ark.v1.Intent 1, // 16: ark.v1.Conviction.type:type_name -> ark.v1.ConvictionType 0, // 17: ark.v1.Conviction.crime_type:type_name -> ark.v1.CrimeType - 56, // 18: ark.v1.ListTokensResponse.tokens:type_name -> ark.v1.TokenInfo - 2, // 19: ark.v1.AdminService.GetScheduledSweep:input_type -> ark.v1.GetScheduledSweepRequest - 4, // 20: ark.v1.AdminService.GetRoundDetails:input_type -> ark.v1.GetRoundDetailsRequest - 6, // 21: ark.v1.AdminService.GetRounds:input_type -> ark.v1.GetRoundsRequest - 8, // 22: ark.v1.AdminService.CreateNote:input_type -> ark.v1.CreateNoteRequest - 10, // 23: ark.v1.AdminService.GetScheduledSessionConfig:input_type -> ark.v1.GetScheduledSessionConfigRequest - 12, // 24: ark.v1.AdminService.UpdateScheduledSessionConfig:input_type -> ark.v1.UpdateScheduledSessionConfigRequest - 14, // 25: ark.v1.AdminService.ClearScheduledSessionConfig:input_type -> ark.v1.ClearScheduledSessionConfigRequest - 16, // 26: ark.v1.AdminService.ListIntents:input_type -> ark.v1.ListIntentsRequest - 18, // 27: ark.v1.AdminService.DeleteIntents:input_type -> ark.v1.DeleteIntentsRequest - 20, // 28: ark.v1.AdminService.GetIntentFees:input_type -> ark.v1.GetIntentFeesRequest - 22, // 29: ark.v1.AdminService.UpdateIntentFees:input_type -> ark.v1.UpdateIntentFeesRequest - 24, // 30: ark.v1.AdminService.ClearIntentFees:input_type -> ark.v1.ClearIntentFeesRequest - 26, // 31: ark.v1.AdminService.GetConvictions:input_type -> ark.v1.GetConvictionsRequest - 28, // 32: ark.v1.AdminService.GetConvictionsInRange:input_type -> ark.v1.GetConvictionsInRangeRequest - 30, // 33: ark.v1.AdminService.GetConvictionsByRound:input_type -> ark.v1.GetConvictionsByRoundRequest - 32, // 34: ark.v1.AdminService.GetActiveScriptConvictions:input_type -> ark.v1.GetActiveScriptConvictionsRequest - 34, // 35: ark.v1.AdminService.PardonConviction:input_type -> ark.v1.PardonConvictionRequest - 36, // 36: ark.v1.AdminService.BanScript:input_type -> ark.v1.BanScriptRequest - 38, // 37: ark.v1.AdminService.RevokeAuth:input_type -> ark.v1.RevokeAuthRequest - 54, // 38: ark.v1.AdminService.ListTokens:input_type -> ark.v1.ListTokensRequest - 57, // 39: ark.v1.AdminService.RevokeTokens:input_type -> ark.v1.RevokeTokensRequest - 48, // 40: ark.v1.AdminService.GetExpiringLiquidity:input_type -> ark.v1.GetExpiringLiquidityRequest - 50, // 41: ark.v1.AdminService.GetRecoverableLiquidity:input_type -> ark.v1.GetRecoverableLiquidityRequest - 52, // 42: ark.v1.AdminService.Sweep:input_type -> ark.v1.SweepRequest - 3, // 43: ark.v1.AdminService.GetScheduledSweep:output_type -> ark.v1.GetScheduledSweepResponse - 5, // 44: ark.v1.AdminService.GetRoundDetails:output_type -> ark.v1.GetRoundDetailsResponse - 7, // 45: ark.v1.AdminService.GetRounds:output_type -> ark.v1.GetRoundsResponse - 9, // 46: ark.v1.AdminService.CreateNote:output_type -> ark.v1.CreateNoteResponse - 11, // 47: ark.v1.AdminService.GetScheduledSessionConfig:output_type -> ark.v1.GetScheduledSessionConfigResponse - 13, // 48: ark.v1.AdminService.UpdateScheduledSessionConfig:output_type -> ark.v1.UpdateScheduledSessionConfigResponse - 15, // 49: ark.v1.AdminService.ClearScheduledSessionConfig:output_type -> ark.v1.ClearScheduledSessionConfigResponse - 17, // 50: ark.v1.AdminService.ListIntents:output_type -> ark.v1.ListIntentsResponse - 19, // 51: ark.v1.AdminService.DeleteIntents:output_type -> ark.v1.DeleteIntentsResponse - 21, // 52: ark.v1.AdminService.GetIntentFees:output_type -> ark.v1.GetIntentFeesResponse - 23, // 53: ark.v1.AdminService.UpdateIntentFees:output_type -> ark.v1.UpdateIntentFeesResponse - 25, // 54: ark.v1.AdminService.ClearIntentFees:output_type -> ark.v1.ClearIntentFeesResponse - 27, // 55: ark.v1.AdminService.GetConvictions:output_type -> ark.v1.GetConvictionsResponse - 29, // 56: ark.v1.AdminService.GetConvictionsInRange:output_type -> ark.v1.GetConvictionsInRangeResponse - 31, // 57: ark.v1.AdminService.GetConvictionsByRound:output_type -> ark.v1.GetConvictionsByRoundResponse - 33, // 58: ark.v1.AdminService.GetActiveScriptConvictions:output_type -> ark.v1.GetActiveScriptConvictionsResponse - 35, // 59: ark.v1.AdminService.PardonConviction:output_type -> ark.v1.PardonConvictionResponse - 37, // 60: ark.v1.AdminService.BanScript:output_type -> ark.v1.BanScriptResponse - 39, // 61: ark.v1.AdminService.RevokeAuth:output_type -> ark.v1.RevokeAuthResponse - 55, // 62: ark.v1.AdminService.ListTokens:output_type -> ark.v1.ListTokensResponse - 58, // 63: ark.v1.AdminService.RevokeTokens:output_type -> ark.v1.RevokeTokensResponse - 49, // 64: ark.v1.AdminService.GetExpiringLiquidity:output_type -> ark.v1.GetExpiringLiquidityResponse - 51, // 65: ark.v1.AdminService.GetRecoverableLiquidity:output_type -> ark.v1.GetRecoverableLiquidityResponse - 53, // 66: ark.v1.AdminService.Sweep:output_type -> ark.v1.SweepResponse - 43, // [43:67] is the sub-list for method output_type - 19, // [19:43] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 54, // 18: ark.v1.GetSettingsResponse.settings:type_name -> ark.v1.Settings + 54, // 19: ark.v1.UpdateSettingsRequest.settings:type_name -> ark.v1.Settings + 63, // 20: ark.v1.ListTokensResponse.tokens:type_name -> ark.v1.TokenInfo + 2, // 21: ark.v1.AdminService.GetScheduledSweep:input_type -> ark.v1.GetScheduledSweepRequest + 4, // 22: ark.v1.AdminService.GetRoundDetails:input_type -> ark.v1.GetRoundDetailsRequest + 6, // 23: ark.v1.AdminService.GetRounds:input_type -> ark.v1.GetRoundsRequest + 8, // 24: ark.v1.AdminService.CreateNote:input_type -> ark.v1.CreateNoteRequest + 10, // 25: ark.v1.AdminService.GetScheduledSessionConfig:input_type -> ark.v1.GetScheduledSessionConfigRequest + 12, // 26: ark.v1.AdminService.UpdateScheduledSessionConfig:input_type -> ark.v1.UpdateScheduledSessionConfigRequest + 14, // 27: ark.v1.AdminService.ClearScheduledSessionConfig:input_type -> ark.v1.ClearScheduledSessionConfigRequest + 16, // 28: ark.v1.AdminService.ListIntents:input_type -> ark.v1.ListIntentsRequest + 18, // 29: ark.v1.AdminService.DeleteIntents:input_type -> ark.v1.DeleteIntentsRequest + 20, // 30: ark.v1.AdminService.GetIntentFees:input_type -> ark.v1.GetIntentFeesRequest + 22, // 31: ark.v1.AdminService.UpdateIntentFees:input_type -> ark.v1.UpdateIntentFeesRequest + 24, // 32: ark.v1.AdminService.ClearIntentFees:input_type -> ark.v1.ClearIntentFeesRequest + 26, // 33: ark.v1.AdminService.GetConvictions:input_type -> ark.v1.GetConvictionsRequest + 28, // 34: ark.v1.AdminService.GetConvictionsInRange:input_type -> ark.v1.GetConvictionsInRangeRequest + 30, // 35: ark.v1.AdminService.GetConvictionsByRound:input_type -> ark.v1.GetConvictionsByRoundRequest + 32, // 36: ark.v1.AdminService.GetActiveScriptConvictions:input_type -> ark.v1.GetActiveScriptConvictionsRequest + 34, // 37: ark.v1.AdminService.PardonConviction:input_type -> ark.v1.PardonConvictionRequest + 36, // 38: ark.v1.AdminService.BanScript:input_type -> ark.v1.BanScriptRequest + 38, // 39: ark.v1.AdminService.RevokeAuth:input_type -> ark.v1.RevokeAuthRequest + 61, // 40: ark.v1.AdminService.ListTokens:input_type -> ark.v1.ListTokensRequest + 64, // 41: ark.v1.AdminService.RevokeTokens:input_type -> ark.v1.RevokeTokensRequest + 48, // 42: ark.v1.AdminService.GetExpiringLiquidity:input_type -> ark.v1.GetExpiringLiquidityRequest + 50, // 43: ark.v1.AdminService.GetRecoverableLiquidity:input_type -> ark.v1.GetRecoverableLiquidityRequest + 52, // 44: ark.v1.AdminService.Sweep:input_type -> ark.v1.SweepRequest + 55, // 45: ark.v1.AdminService.GetSettings:input_type -> ark.v1.GetSettingsRequest + 57, // 46: ark.v1.AdminService.UpdateSettings:input_type -> ark.v1.UpdateSettingsRequest + 59, // 47: ark.v1.AdminService.ClearSettings:input_type -> ark.v1.ClearSettingsRequest + 3, // 48: ark.v1.AdminService.GetScheduledSweep:output_type -> ark.v1.GetScheduledSweepResponse + 5, // 49: ark.v1.AdminService.GetRoundDetails:output_type -> ark.v1.GetRoundDetailsResponse + 7, // 50: ark.v1.AdminService.GetRounds:output_type -> ark.v1.GetRoundsResponse + 9, // 51: ark.v1.AdminService.CreateNote:output_type -> ark.v1.CreateNoteResponse + 11, // 52: ark.v1.AdminService.GetScheduledSessionConfig:output_type -> ark.v1.GetScheduledSessionConfigResponse + 13, // 53: ark.v1.AdminService.UpdateScheduledSessionConfig:output_type -> ark.v1.UpdateScheduledSessionConfigResponse + 15, // 54: ark.v1.AdminService.ClearScheduledSessionConfig:output_type -> ark.v1.ClearScheduledSessionConfigResponse + 17, // 55: ark.v1.AdminService.ListIntents:output_type -> ark.v1.ListIntentsResponse + 19, // 56: ark.v1.AdminService.DeleteIntents:output_type -> ark.v1.DeleteIntentsResponse + 21, // 57: ark.v1.AdminService.GetIntentFees:output_type -> ark.v1.GetIntentFeesResponse + 23, // 58: ark.v1.AdminService.UpdateIntentFees:output_type -> ark.v1.UpdateIntentFeesResponse + 25, // 59: ark.v1.AdminService.ClearIntentFees:output_type -> ark.v1.ClearIntentFeesResponse + 27, // 60: ark.v1.AdminService.GetConvictions:output_type -> ark.v1.GetConvictionsResponse + 29, // 61: ark.v1.AdminService.GetConvictionsInRange:output_type -> ark.v1.GetConvictionsInRangeResponse + 31, // 62: ark.v1.AdminService.GetConvictionsByRound:output_type -> ark.v1.GetConvictionsByRoundResponse + 33, // 63: ark.v1.AdminService.GetActiveScriptConvictions:output_type -> ark.v1.GetActiveScriptConvictionsResponse + 35, // 64: ark.v1.AdminService.PardonConviction:output_type -> ark.v1.PardonConvictionResponse + 37, // 65: ark.v1.AdminService.BanScript:output_type -> ark.v1.BanScriptResponse + 39, // 66: ark.v1.AdminService.RevokeAuth:output_type -> ark.v1.RevokeAuthResponse + 62, // 67: ark.v1.AdminService.ListTokens:output_type -> ark.v1.ListTokensResponse + 65, // 68: ark.v1.AdminService.RevokeTokens:output_type -> ark.v1.RevokeTokensResponse + 49, // 69: ark.v1.AdminService.GetExpiringLiquidity:output_type -> ark.v1.GetExpiringLiquidityResponse + 51, // 70: ark.v1.AdminService.GetRecoverableLiquidity:output_type -> ark.v1.GetRecoverableLiquidityResponse + 53, // 71: ark.v1.AdminService.Sweep:output_type -> ark.v1.SweepResponse + 56, // 72: ark.v1.AdminService.GetSettings:output_type -> ark.v1.GetSettingsResponse + 58, // 73: ark.v1.AdminService.UpdateSettings:output_type -> ark.v1.UpdateSettingsResponse + 60, // 74: ark.v1.AdminService.ClearSettings:output_type -> ark.v1.ClearSettingsResponse + 48, // [48:75] is the sub-list for method output_type + 21, // [21:48] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_ark_v1_admin_proto_init() } @@ -3442,7 +3906,7 @@ func file_ark_v1_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ark_v1_admin_proto_rawDesc), len(file_ark_v1_admin_proto_rawDesc)), NumEnums: 2, - NumMessages: 57, + NumMessages: 64, NumExtensions: 0, NumServices: 1, }, diff --git a/api-spec/protobuf/gen/ark/v1/admin.pb.rgw.go b/api-spec/protobuf/gen/ark/v1/admin.pb.rgw.go index 4fe729b14..3d16b2755 100644 --- a/api-spec/protobuf/gen/ark/v1/admin.pb.rgw.go +++ b/api-spec/protobuf/gen/ark/v1/admin.pb.rgw.go @@ -417,6 +417,41 @@ func request_AdminService_Sweep_0(ctx context.Context, marshaler gateway.Marshal } +func request_AdminService_GetSettings_0(ctx context.Context, marshaler gateway.Marshaler, mux *gateway.ServeMux, client AdminServiceClient, req *http.Request, pathParams gateway.Params) (proto.Message, gateway.ServerMetadata, error) { + var protoReq GetSettingsRequest + var metadata gateway.ServerMetadata + + msg, err := client.GetSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func request_AdminService_UpdateSettings_0(ctx context.Context, marshaler gateway.Marshaler, mux *gateway.ServeMux, client AdminServiceClient, req *http.Request, pathParams gateway.Params) (proto.Message, gateway.ServerMetadata, error) { + var protoReq UpdateSettingsRequest + var metadata gateway.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, gateway.ErrMarshal{Err: err, Inbound: true} + } + + msg, err := client.UpdateSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func request_AdminService_ClearSettings_0(ctx context.Context, marshaler gateway.Marshaler, mux *gateway.ServeMux, client AdminServiceClient, req *http.Request, pathParams gateway.Params) (proto.Message, gateway.ServerMetadata, error) { + var protoReq ClearSettingsRequest + var metadata gateway.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, gateway.ErrMarshal{Err: err, Inbound: true} + } + + msg, err := client.ClearSettings(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + // RegisterAdminServiceHandlerFromEndpoint is same as RegisterAdminServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterAdminServiceHandlerFromEndpoint(ctx context.Context, mux *gateway.ServeMux, endpoint string, opts []grpc.DialOption) error { @@ -984,4 +1019,70 @@ func RegisterAdminServiceHandlerClient(ctx context.Context, mux *gateway.ServeMu mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) }) + mux.HandleWithParams("GET", "/v1/admin/settings", func(w http.ResponseWriter, req *http.Request, pathParams gateway.Params) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := mux.MarshalerForRequest(req) + var err error + var annotatedContext context.Context + annotatedContext, err = gateway.AnnotateContext(ctx, mux, req, "/ark.v1.AdminService/GetSettings", gateway.WithHTTPPathPattern("/v1/admin/settings")) + if err != nil { + mux.HTTPError(ctx, outboundMarshaler, w, req, err) + return + } + + resp, md, err := request_AdminService_GetSettings_0(annotatedContext, inboundMarshaler, mux, client, req, pathParams) + annotatedContext = gateway.NewServerMetadataContext(annotatedContext, md) + if err != nil { + mux.HTTPError(annotatedContext, outboundMarshaler, w, req, err) + return + } + + mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) + }) + + mux.HandleWithParams("POST", "/v1/admin/settings", func(w http.ResponseWriter, req *http.Request, pathParams gateway.Params) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := mux.MarshalerForRequest(req) + var err error + var annotatedContext context.Context + annotatedContext, err = gateway.AnnotateContext(ctx, mux, req, "/ark.v1.AdminService/UpdateSettings", gateway.WithHTTPPathPattern("/v1/admin/settings")) + if err != nil { + mux.HTTPError(ctx, outboundMarshaler, w, req, err) + return + } + + resp, md, err := request_AdminService_UpdateSettings_0(annotatedContext, inboundMarshaler, mux, client, req, pathParams) + annotatedContext = gateway.NewServerMetadataContext(annotatedContext, md) + if err != nil { + mux.HTTPError(annotatedContext, outboundMarshaler, w, req, err) + return + } + + mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) + }) + + mux.HandleWithParams("POST", "/v1/admin/settings/clear", func(w http.ResponseWriter, req *http.Request, pathParams gateway.Params) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := mux.MarshalerForRequest(req) + var err error + var annotatedContext context.Context + annotatedContext, err = gateway.AnnotateContext(ctx, mux, req, "/ark.v1.AdminService/ClearSettings", gateway.WithHTTPPathPattern("/v1/admin/settings/clear")) + if err != nil { + mux.HTTPError(ctx, outboundMarshaler, w, req, err) + return + } + + resp, md, err := request_AdminService_ClearSettings_0(annotatedContext, inboundMarshaler, mux, client, req, pathParams) + annotatedContext = gateway.NewServerMetadataContext(annotatedContext, md) + if err != nil { + mux.HTTPError(annotatedContext, outboundMarshaler, w, req, err) + return + } + + mux.ForwardResponseMessage(annotatedContext, outboundMarshaler, w, req, resp) + }) + } diff --git a/api-spec/protobuf/gen/ark/v1/admin_grpc.pb.go b/api-spec/protobuf/gen/ark/v1/admin_grpc.pb.go index b2c1001f9..7f293eea6 100644 --- a/api-spec/protobuf/gen/ark/v1/admin_grpc.pb.go +++ b/api-spec/protobuf/gen/ark/v1/admin_grpc.pb.go @@ -43,6 +43,9 @@ const ( AdminService_GetExpiringLiquidity_FullMethodName = "/ark.v1.AdminService/GetExpiringLiquidity" AdminService_GetRecoverableLiquidity_FullMethodName = "/ark.v1.AdminService/GetRecoverableLiquidity" AdminService_Sweep_FullMethodName = "/ark.v1.AdminService/Sweep" + AdminService_GetSettings_FullMethodName = "/ark.v1.AdminService/GetSettings" + AdminService_UpdateSettings_FullMethodName = "/ark.v1.AdminService/UpdateSettings" + AdminService_ClearSettings_FullMethodName = "/ark.v1.AdminService/ClearSettings" ) // AdminServiceClient is the client API for AdminService service. @@ -73,6 +76,9 @@ type AdminServiceClient interface { GetExpiringLiquidity(ctx context.Context, in *GetExpiringLiquidityRequest, opts ...grpc.CallOption) (*GetExpiringLiquidityResponse, error) GetRecoverableLiquidity(ctx context.Context, in *GetRecoverableLiquidityRequest, opts ...grpc.CallOption) (*GetRecoverableLiquidityResponse, error) Sweep(ctx context.Context, in *SweepRequest, opts ...grpc.CallOption) (*SweepResponse, error) + GetSettings(ctx context.Context, in *GetSettingsRequest, opts ...grpc.CallOption) (*GetSettingsResponse, error) + UpdateSettings(ctx context.Context, in *UpdateSettingsRequest, opts ...grpc.CallOption) (*UpdateSettingsResponse, error) + ClearSettings(ctx context.Context, in *ClearSettingsRequest, opts ...grpc.CallOption) (*ClearSettingsResponse, error) } type adminServiceClient struct { @@ -323,6 +329,36 @@ func (c *adminServiceClient) Sweep(ctx context.Context, in *SweepRequest, opts . return out, nil } +func (c *adminServiceClient) GetSettings(ctx context.Context, in *GetSettingsRequest, opts ...grpc.CallOption) (*GetSettingsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSettingsResponse) + err := c.cc.Invoke(ctx, AdminService_GetSettings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminServiceClient) UpdateSettings(ctx context.Context, in *UpdateSettingsRequest, opts ...grpc.CallOption) (*UpdateSettingsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateSettingsResponse) + err := c.cc.Invoke(ctx, AdminService_UpdateSettings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminServiceClient) ClearSettings(ctx context.Context, in *ClearSettingsRequest, opts ...grpc.CallOption) (*ClearSettingsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClearSettingsResponse) + err := c.cc.Invoke(ctx, AdminService_ClearSettings_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AdminServiceServer is the server API for AdminService service. // All implementations should embed UnimplementedAdminServiceServer // for forward compatibility. @@ -351,6 +387,9 @@ type AdminServiceServer interface { GetExpiringLiquidity(context.Context, *GetExpiringLiquidityRequest) (*GetExpiringLiquidityResponse, error) GetRecoverableLiquidity(context.Context, *GetRecoverableLiquidityRequest) (*GetRecoverableLiquidityResponse, error) Sweep(context.Context, *SweepRequest) (*SweepResponse, error) + GetSettings(context.Context, *GetSettingsRequest) (*GetSettingsResponse, error) + UpdateSettings(context.Context, *UpdateSettingsRequest) (*UpdateSettingsResponse, error) + ClearSettings(context.Context, *ClearSettingsRequest) (*ClearSettingsResponse, error) } // UnimplementedAdminServiceServer should be embedded to have @@ -432,6 +471,15 @@ func (UnimplementedAdminServiceServer) GetRecoverableLiquidity(context.Context, func (UnimplementedAdminServiceServer) Sweep(context.Context, *SweepRequest) (*SweepResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Sweep not implemented") } +func (UnimplementedAdminServiceServer) GetSettings(context.Context, *GetSettingsRequest) (*GetSettingsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSettings not implemented") +} +func (UnimplementedAdminServiceServer) UpdateSettings(context.Context, *UpdateSettingsRequest) (*UpdateSettingsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateSettings not implemented") +} +func (UnimplementedAdminServiceServer) ClearSettings(context.Context, *ClearSettingsRequest) (*ClearSettingsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ClearSettings not implemented") +} func (UnimplementedAdminServiceServer) testEmbeddedByValue() {} // UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service. @@ -884,6 +932,60 @@ func _AdminService_Sweep_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _AdminService_GetSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSettingsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).GetSettings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_GetSettings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).GetSettings(ctx, req.(*GetSettingsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminService_UpdateSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateSettingsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).UpdateSettings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_UpdateSettings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).UpdateSettings(ctx, req.(*UpdateSettingsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminService_ClearSettings_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClearSettingsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServiceServer).ClearSettings(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminService_ClearSettings_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServiceServer).ClearSettings(ctx, req.(*ClearSettingsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -987,6 +1089,18 @@ var AdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Sweep", Handler: _AdminService_Sweep_Handler, }, + { + MethodName: "GetSettings", + Handler: _AdminService_GetSettings_Handler, + }, + { + MethodName: "UpdateSettings", + Handler: _AdminService_UpdateSettings_Handler, + }, + { + MethodName: "ClearSettings", + Handler: _AdminService_ClearSettings_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ark/v1/admin.proto", diff --git a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go index 3ee797b0f..c8420ca30 100644 --- a/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go +++ b/api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go @@ -243,7 +243,7 @@ func request_IndexerService_GetVtxos_0(ctx context.Context, marshaler gateway.Ma var ( query_params_IndexerService_GetVtxoChain_0 = gateway.QueryParameterParseOptions{ - Filter: trie.New("outpoint.txid", "outpoint.vout", "txid", "vout"), + Filter: trie.New("txid", "vout", "outpoint.txid", "outpoint.vout"), } ) diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml index 31a91a74c..096aa9c7c 100644 --- a/docker-compose.regtest.yml +++ b/docker-compose.regtest.yml @@ -87,17 +87,23 @@ services: environment: - ARKD_LOG_LEVEL=6 - ARKD_NO_MACAROONS=true + - ARKD_SCHEDULER_TYPE=block + - ARKD_ALLOW_CSV_BLOCK_TYPE=true + # First-boot seed overrides so the DB-backed settings are short enough + # for the e2e harness. Only consulted when the settings table is empty. + # Values match master's regtest defaults (all block-based, same type). - ARKD_VTXO_TREE_EXPIRY=40 - ARKD_UNILATERAL_EXIT_DELAY=20 - ARKD_PUBLIC_UNILATERAL_EXIT_DELAY=20 - ARKD_BOARDING_EXIT_DELAY=30 - ARKD_CHECKPOINT_EXIT_DELAY=10 - - ARKD_DATADIR=./data/regtest - - ARKD_WALLET_ADDR=arkd-wallet:6060 - - ARKD_ESPLORA_URL=http://chopsticks:3000 - ARKD_ROUND_MIN_PARTICIPANTS_COUNT=${ARKD_ROUND_MIN_PARTICIPANTS_COUNT:-1} - ARKD_ROUND_MAX_PARTICIPANTS_COUNT=${ARKD_ROUND_MAX_PARTICIPANTS_COUNT:-128} - ARKD_VTXO_MIN_AMOUNT=1 + - ARKD_BAN_THRESHOLD=1 + - ARKD_DATADIR=./data/regtest + - ARKD_WALLET_ADDR=arkd-wallet:6060 + - ARKD_ESPLORA_URL=http://chopsticks:3000 - ARKD_DB_TYPE=${ARKD_DB_TYPE:-sqlite} - ARKD_PG_DB_URL=${ARKD_PG_DB_URL:-} - ARKD_PG_EVENT_DB_URL=${ARKD_PG_EVENT_DB_URL:-} diff --git a/envs/arkd.dev.env b/envs/arkd.dev.env index 61ad9a157..e93e12131 100644 --- a/envs/arkd.dev.env +++ b/envs/arkd.dev.env @@ -1,10 +1,6 @@ ARKD_LOG_LEVEL=5 ARKD_NO_MACAROONS=true -ARKD_VTXO_TREE_EXPIRY=40 -ARKD_CHECKPOINT_EXIT_DELAY=10 -ARKD_UNILATERAL_EXIT_DELAY=20 -ARKD_PUBLIC_UNILATERAL_EXIT_DELAY=20 -ARKD_BOARDING_EXIT_DELAY=30 +ARKD_SCHEDULER_TYPE=block ARKD_DATADIR=./data/regtest ARKD_ESPLORA_URL=http://localhost:3000 ARKD_WALLET_ADDR=127.0.0.1:6060 @@ -16,8 +12,7 @@ ARKD_PG_DB_MAX_IDLE_CONN=50 ARKD_PG_DB_CONN_MAX_IDLE_MINS=5 ARKD_PG_DB_CONN_MAX_LIFE_MINS=30 ARKD_REDIS_URL=redis://localhost:6379/0 -ARKD_VTXO_MIN_AMOUNT=1 -ARKD_BAN_THRESHOLD=1 +ARKD_ALLOW_CSV_BLOCK_TYPE=true ARKD_ONCHAIN_OUTPUT_FEE=100 ARKD_ENABLE_PPROF=true ARKD_UNROLLED_VTXO_MIN_EXPIRY_MARGIN=10 diff --git a/envs/arkd.light.env b/envs/arkd.light.env index 35c74d3d3..266f9b02a 100644 --- a/envs/arkd.light.env +++ b/envs/arkd.light.env @@ -1,8 +1,5 @@ ARKD_LOG_LEVEL=5 ARKD_NO_MACAROONS=true -ARKD_UNILATERAL_EXIT_DELAY=20 -ARKD_PUBLIC_UNILATERAL_EXIT_DELAY=20 -ARKD_BOARDING_EXIT_DELAY=30 ARKD_DATADIR=./data/regtest ARKD_ESPLORA_URL=http://localhost:3000 ARKD_WALLET_ADDR=127.0.0.1:6060 diff --git a/internal/config/config.go b/internal/config/config.go index b1028536d..cff7df63a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( "time" "github.com/arkade-os/arkd/internal/core/application" + "github.com/arkade-os/arkd/internal/core/domain" "github.com/arkade-os/arkd/internal/core/ports" alertsmanager "github.com/arkade-os/arkd/internal/infrastructure/alertsmanager" "github.com/arkade-os/arkd/internal/infrastructure/db" @@ -34,7 +35,6 @@ import ( ) const ( - minAllowedSequence = 512 bitcoinBlockWeight = 4_000_000 ) @@ -91,6 +91,7 @@ type Config struct { SessionDuration int64 BanDuration int64 BanThreshold int64 // number of crimes to trigger a ban + SchedulerType string TxBuilderType string LiveStoreType string RedisUrl string @@ -184,8 +185,6 @@ var ( WalletAddr = "WALLET_ADDR" SignerAddr = "SIGNER_ADDR" SessionDuration = "SESSION_DURATION" - BanDuration = "BAN_DURATION" - BanThreshold = "BAN_THRESHOLD" Port = "PORT" AdminPort = "ADMIN_PORT" EventDbType = "EVENT_DB_TYPE" @@ -201,12 +200,8 @@ var ( LiveStoreType = "LIVE_STORE_TYPE" RedisUrl = "REDIS_URL" RedisTxNumOfRetries = "REDIS_NUM_OF_RETRIES" + SchedulerType = "SCHEDULER_TYPE" LogLevel = "LOG_LEVEL" - VtxoTreeExpiry = "VTXO_TREE_EXPIRY" - UnilateralExitDelay = "UNILATERAL_EXIT_DELAY" - PublicUnilateralExitDelay = "PUBLIC_UNILATERAL_EXIT_DELAY" - CheckpointExitDelay = "CHECKPOINT_EXIT_DELAY" - BoardingExitDelay = "BOARDING_EXIT_DELAY" EsploraURL = "ESPLORA_URL" AlertManagerURL = "ALERT_MANAGER_URL" ArkadeExplorerURL = "ARKADE_EXPLORER_URL" @@ -253,10 +248,19 @@ var ( IndexerSigningKey = "INDEXER_SIGNING_PRIVKEY" // #nosec G101 MaxConcurrentStreams = "MAX_CONCURRENT_STREAMS" + // First-boot seed overrides for DB-backed settings. When no settings row + // exists yet, defaultSettings() reads these env vars; at runtime the DB + // (mutated via the admin UpdateSettings API) is the source of truth. + BanThreshold = "BAN_THRESHOLD" + BanDuration = "BAN_DURATION" + VtxoTreeExpiry = "VTXO_TREE_EXPIRY" + UnilateralExitDelay = "UNILATERAL_EXIT_DELAY" + PublicUnilateralExitDelay = "PUBLIC_UNILATERAL_EXIT_DELAY" + CheckpointExitDelay = "CHECKPOINT_EXIT_DELAY" + BoardingExitDelay = "BOARDING_EXIT_DELAY" + defaultDatadir = arklib.AppDataDir("arkd", false) defaultSessionDuration = 30 - defaultBanDuration = 10 * defaultSessionDuration - defaultBanThreshold = 3 DefaultPort = 7070 DefaultAdminPort = 7071 defaultDbType = "postgres" @@ -267,32 +271,22 @@ var ( defaultEsploraURL = "https://blockstream.info/api" defaultArkadeExplorerURL = "https://arkade.space" defaultLogLevel = 4 - defaultVtxoTreeExpiry = 604672 // 7 days - defaultUnilateralExitDelay = 86400 // 24 hours - defaultCheckpointExitDelay = 86400 // 24 hours - defaultBoardingExitDelay = 7776000 // 3 months defaultNoMacaroons = false defaultNoTLS = true - defaultUtxoMaxAmount = -1 // -1 means no limit (default), 0 means boarding not allowed - defaultUtxoMinAmount = -1 // -1 means native dust limit (default) - defaultVtxoMinAmount = -1 // -1 means native dust limit (default) - defaultVtxoMaxAmount = -1 // -1 means no limit (default) - - defaultRoundMaxParticipantsCount = 128 - defaultRoundMinParticipantsCount = 1 - defaultOtelPushInterval = 10 // seconds - defaultHeartbeatInterval = 60 // seconds - defaultRoundReportServiceEnabled = false - defaultSettlementMinExpiryGap = 0 // disabled by default - defaultUnrolledVtxoMinExpiryMargin = 300 // 5 minutes in seconds - defaultMaxTxWeight = int64(0.01 * bitcoinBlockWeight) - defaultAssetTxMaxWeightRatio = 0.5 - defaultVtxoNoCsvValidationCutoffDate = 0 // disabled by default - defaultEnablePprof = false - defaultIndexerExposure = "public" - defaultIndexerAuthTokenExpiry = 300 // 5 minutes in seconds - defaultMaxConcurrentStreams = uint32(1000) - defaultMaxOpReturnOuts = uint32(3) + defaultSchedulerType = "gocron" + + defaultOtelPushInterval = 10 // seconds + defaultHeartbeatInterval = 60 // seconds + defaultRoundReportServiceEnabled = false + defaultSettlementMinExpiryGap = 0 // disabled by default + defaultUnrolledVtxoMinExpiryMargin = 300 // 5 minutes in seconds + defaultMaxTxWeight = int64(0.01 * bitcoinBlockWeight) + defaultAssetTxMaxWeightRatio = 0.5 + defaultEnablePprof = false + defaultIndexerExposure = "public" + defaultIndexerAuthTokenExpiry = 300 // 5 minutes in seconds + defaultMaxConcurrentStreams = uint32(1000) + defaultMaxOpReturnOuts = uint32(3) ) func LoadConfig() (*Config, error) { @@ -310,24 +304,12 @@ func LoadConfig() (*Config, error) { viper.SetDefault(NoTLS, defaultNoTLS) viper.SetDefault(LogLevel, defaultLogLevel) viper.SetDefault(SessionDuration, defaultSessionDuration) - viper.SetDefault(BanDuration, defaultBanDuration) - viper.SetDefault(BanThreshold, defaultBanThreshold) - viper.SetDefault(VtxoTreeExpiry, defaultVtxoTreeExpiry) + viper.SetDefault(SchedulerType, defaultSchedulerType) viper.SetDefault(EventDbType, defaultEventDbType) viper.SetDefault(TxBuilderType, defaultTxBuilderType) - viper.SetDefault(UnilateralExitDelay, defaultUnilateralExitDelay) - viper.SetDefault(PublicUnilateralExitDelay, defaultUnilateralExitDelay) - viper.SetDefault(CheckpointExitDelay, defaultCheckpointExitDelay) viper.SetDefault(EsploraURL, defaultEsploraURL) viper.SetDefault(ArkadeExplorerURL, defaultArkadeExplorerURL) viper.SetDefault(NoMacaroons, defaultNoMacaroons) - viper.SetDefault(BoardingExitDelay, defaultBoardingExitDelay) - viper.SetDefault(RoundMaxParticipantsCount, defaultRoundMaxParticipantsCount) - viper.SetDefault(RoundMinParticipantsCount, defaultRoundMinParticipantsCount) - viper.SetDefault(UtxoMaxAmount, defaultUtxoMaxAmount) - viper.SetDefault(UtxoMinAmount, defaultUtxoMinAmount) - viper.SetDefault(VtxoMaxAmount, defaultVtxoMaxAmount) - viper.SetDefault(VtxoMinAmount, defaultVtxoMinAmount) viper.SetDefault(LiveStoreType, defaultLiveStoreType) viper.SetDefault(RedisTxNumOfRetries, defaultRedisTxNumOfRetries) viper.SetDefault(OtelPushInterval, defaultOtelPushInterval) @@ -337,13 +319,20 @@ func LoadConfig() (*Config, error) { viper.SetDefault(UnrolledVtxoMinExpiryMargin, defaultUnrolledVtxoMinExpiryMargin) viper.SetDefault(MaxTxWeight, defaultMaxTxWeight) viper.SetDefault(AssetTxMaxWeightRatio, defaultAssetTxMaxWeightRatio) - viper.SetDefault(VtxoNoCsvValidationCutoffDate, defaultVtxoNoCsvValidationCutoffDate) viper.SetDefault(EnablePprof, defaultEnablePprof) viper.SetDefault(IndexerExposure, defaultIndexerExposure) viper.SetDefault(IndexerAuthTokenExpiry, defaultIndexerAuthTokenExpiry) viper.SetDefault(MaxConcurrentStreams, defaultMaxConcurrentStreams) viper.SetDefault(MaxOpReturnOutputs, defaultMaxOpReturnOuts) + // First-boot seed defaults for DB-backed settings (production values). + // Only consulted in defaultSettings() when no settings row exists yet. + viper.SetDefault(BanThreshold, 3) + viper.SetDefault(BanDuration, 300) // 10 * 30s + viper.SetDefault(UnilateralExitDelay, 86400) // 24 hours + viper.SetDefault(PublicUnilateralExitDelay, 86400) // 24 hours + viper.SetDefault(BoardingExitDelay, 7776000) // 3 months + if err := initDatadir(); err != nil { return nil, fmt.Errorf("failed to create datadir: %s", err) } @@ -390,8 +379,6 @@ func LoadConfig() (*Config, error) { WalletAddr: viper.GetString(WalletAddr), SignerAddr: signerAddr, SessionDuration: viper.GetInt64(SessionDuration), - BanDuration: viper.GetInt64(BanDuration), - BanThreshold: viper.GetInt64(BanThreshold), Port: viper.GetUint32(Port), AdminPort: adminPort, EventDbType: viper.GetString(EventDbType), @@ -411,11 +398,6 @@ func LoadConfig() (*Config, error) { PostgresConnMaxIdleMins: viper.GetInt64(PostgresConnMaxIdleMins), PostgresConnMaxLifeMins: viper.GetInt64(PostgresConnMaxLifeMins), LogLevel: viper.GetInt(LogLevel), - VtxoTreeExpiry: determineLocktimeType(viper.GetInt64(VtxoTreeExpiry)), - UnilateralExitDelay: determineLocktimeType(viper.GetInt64(UnilateralExitDelay)), - PublicUnilateralExitDelay: determineLocktimeType(viper.GetInt64(PublicUnilateralExitDelay)), - CheckpointExitDelay: determineLocktimeType(viper.GetInt64(CheckpointExitDelay)), - BoardingExitDelay: determineLocktimeType(viper.GetInt64(BoardingExitDelay)), EsploraURL: viper.GetString(EsploraURL), AlertManagerURL: viper.GetString(AlertManagerURL), ArkadeExplorerURL: viper.GetString(ArkadeExplorerURL), @@ -476,11 +458,118 @@ func makeDirectoryIfNotExists(path string) error { } func determineLocktimeType(locktime int64) arklib.RelativeLocktime { - if locktime >= minAllowedSequence { - return arklib.RelativeLocktime{Type: arklib.LocktimeTypeSecond, Value: uint32(locktime)} + return domain.ToRelativeLocktime(locktime) +} + +// defaultSettings builds the first-boot seed for DB-backed settings. It is +// only consulted when the settings table is empty. Env vars (ARKD_*) override +// the hardcoded production defaults so test environments and operators can +// seed the DB deterministically before the round scheduler comes online. +// After the first boot, this function is no longer consulted — the admin +// UpdateSettings API is the only way to mutate the stored settings. +func (c *Config) defaultSettings() *domain.Settings { + // Scheduler-aware fallbacks for the two fields that have no env var: + // with the block scheduler we want short block-based delays, otherwise + // reasonable second-based production defaults. + vtxoTreeExpiry := int64(604672) // ~7 days in seconds + checkpointExitDelay := int64(86400) // 24 hours in seconds + if c.SchedulerType == "block" { + vtxoTreeExpiry = 20 // 20 blocks + checkpointExitDelay = 10 // 10 blocks + } + if viper.IsSet(VtxoTreeExpiry) { + vtxoTreeExpiry = viper.GetInt64(VtxoTreeExpiry) + } + if viper.IsSet(CheckpointExitDelay) { + checkpointExitDelay = viper.GetInt64(CheckpointExitDelay) + } + + // Wiring env-var reads for the remaining fields that already had env + // var constants prior to the DB-backed-settings refactor. + roundMin := int64(1) + if viper.IsSet(RoundMinParticipantsCount) { + roundMin = viper.GetInt64(RoundMinParticipantsCount) + } + roundMax := int64(128) + if viper.IsSet(RoundMaxParticipantsCount) { + roundMax = viper.GetInt64(RoundMaxParticipantsCount) + } + vtxoMin := int64(-1) + if viper.IsSet(VtxoMinAmount) { + vtxoMin = viper.GetInt64(VtxoMinAmount) + } + vtxoMax := int64(-1) + if viper.IsSet(VtxoMaxAmount) { + vtxoMax = viper.GetInt64(VtxoMaxAmount) + } + utxoMin := int64(-1) + if viper.IsSet(UtxoMinAmount) { + utxoMin = viper.GetInt64(UtxoMinAmount) + } + utxoMax := int64(-1) + if viper.IsSet(UtxoMaxAmount) { + utxoMax = viper.GetInt64(UtxoMaxAmount) + } + maxTxWeight := int64(0.01 * bitcoinBlockWeight) + if viper.IsSet(MaxTxWeight) { + maxTxWeight = viper.GetInt64(MaxTxWeight) + } + + return &domain.Settings{ + BanThreshold: viper.GetInt64(BanThreshold), + BanDuration: viper.GetInt64(BanDuration), + VtxoTreeExpiry: vtxoTreeExpiry, + UnilateralExitDelay: viper.GetInt64(UnilateralExitDelay), + PublicUnilateralExitDelay: viper.GetInt64(PublicUnilateralExitDelay), + CheckpointExitDelay: checkpointExitDelay, + BoardingExitDelay: viper.GetInt64(BoardingExitDelay), + RoundMinParticipantsCount: roundMin, + RoundMaxParticipantsCount: roundMax, + UtxoMaxAmount: utxoMax, + UtxoMinAmount: utxoMin, + VtxoMaxAmount: vtxoMax, + VtxoMinAmount: vtxoMin, + MaxTxWeight: maxTxWeight, + UpdatedAt: time.Now(), } +} - return arklib.RelativeLocktime{Type: arklib.LocktimeTypeBlock, Value: uint32(locktime)} +func (c *Config) loadSettings() error { + ctx := context.Background() + settings, err := c.repo.Settings().Get(ctx) + if err != nil { + return fmt.Errorf("failed to get settings: %w", err) + } + if settings == nil { + settings = c.defaultSettings() + // Run the same validation the admin UpdateSettings path enforces so a + // bad env-var combination (e.g. PublicUnilateral < Unilateral, + // RoundMin > RoundMax) can't be silently persisted on first boot. + if err := settings.Validate(); err != nil { + return fmt.Errorf("invalid default settings: %w", err) + } + if err := c.repo.Settings().Upsert(ctx, *settings); err != nil { + return fmt.Errorf("failed to seed default settings: %w", err) + } + } + + c.BanThreshold = settings.BanThreshold + c.BanDuration = settings.BanDuration + c.VtxoTreeExpiry = determineLocktimeType(settings.VtxoTreeExpiry) + c.UnilateralExitDelay = determineLocktimeType(settings.UnilateralExitDelay) + c.PublicUnilateralExitDelay = determineLocktimeType(settings.PublicUnilateralExitDelay) + c.CheckpointExitDelay = determineLocktimeType(settings.CheckpointExitDelay) + c.BoardingExitDelay = determineLocktimeType(settings.BoardingExitDelay) + c.RoundMinParticipantsCount = settings.RoundMinParticipantsCount + c.RoundMaxParticipantsCount = settings.RoundMaxParticipantsCount + c.VtxoMinAmount = settings.VtxoMinAmount + c.VtxoMaxAmount = settings.VtxoMaxAmount + c.UtxoMinAmount = settings.UtxoMinAmount + c.UtxoMaxAmount = settings.UtxoMaxAmount + c.SettlementMinExpiryGap = settings.SettlementMinExpiryGap + c.VtxoNoCsvValidationCutoffDate = settings.VtxoNoCsvValidationCutoffDate + c.MaxTxWeight = uint64(settings.MaxTxWeight) + return nil } func (c *Config) Validate() error { @@ -527,6 +616,14 @@ func (c *Config) Validate() error { c.UnrolledVtxoMinExpiryMargin, c.SessionDuration, ) } + + if err := c.repoManager(); err != nil { + return err + } + if err := c.loadSettings(); err != nil { + return err + } + if c.BanDuration < 1 { return fmt.Errorf("invalid ban duration, must be at least 1 second") } @@ -562,55 +659,55 @@ func (c *Config) Validate() error { ) } - // Round seconds-based delays to multiples of minAllowedSequence (BIP68 requirement). + // Round seconds-based delays to multiples of domain.MinAllowedSequence (BIP68 requirement). // Block-based delays don't need rounding. if c.VtxoTreeExpiry.Type == arklib.LocktimeTypeSecond { // vtxo tree expiry must be a multiple of 512 if expressed in seconds - if c.VtxoTreeExpiry.Value%minAllowedSequence != 0 { - c.VtxoTreeExpiry.Value -= c.VtxoTreeExpiry.Value % minAllowedSequence + if c.VtxoTreeExpiry.Value%domain.MinAllowedSequence != 0 { + c.VtxoTreeExpiry.Value -= c.VtxoTreeExpiry.Value % domain.MinAllowedSequence log.Infof( "vtxo tree expiry must be a multiple of %d, rounded to %d", - minAllowedSequence, c.VtxoTreeExpiry, + domain.MinAllowedSequence, c.VtxoTreeExpiry, ) } } if c.CheckpointExitDelay.Type == arklib.LocktimeTypeSecond { - if c.CheckpointExitDelay.Value%minAllowedSequence != 0 { - c.CheckpointExitDelay.Value -= c.CheckpointExitDelay.Value % minAllowedSequence + if c.CheckpointExitDelay.Value%domain.MinAllowedSequence != 0 { + c.CheckpointExitDelay.Value -= c.CheckpointExitDelay.Value % domain.MinAllowedSequence log.Infof( "checkpoint exit delay must be a multiple of %d, rounded to %d", - minAllowedSequence, c.CheckpointExitDelay, + domain.MinAllowedSequence, c.CheckpointExitDelay, ) } } if c.UnilateralExitDelay.Type == arklib.LocktimeTypeSecond { - if c.UnilateralExitDelay.Value%minAllowedSequence != 0 { - c.UnilateralExitDelay.Value -= c.UnilateralExitDelay.Value % minAllowedSequence + if c.UnilateralExitDelay.Value%domain.MinAllowedSequence != 0 { + c.UnilateralExitDelay.Value -= c.UnilateralExitDelay.Value % domain.MinAllowedSequence log.Infof( "unilateral exit delay must be a multiple of %d, rounded to %d", - minAllowedSequence, c.UnilateralExitDelay, + domain.MinAllowedSequence, c.UnilateralExitDelay, ) } } if c.PublicUnilateralExitDelay.Type == arklib.LocktimeTypeSecond { - if c.PublicUnilateralExitDelay.Value%minAllowedSequence != 0 { - c.PublicUnilateralExitDelay.Value -= c.PublicUnilateralExitDelay.Value % minAllowedSequence + if c.PublicUnilateralExitDelay.Value%domain.MinAllowedSequence != 0 { + c.PublicUnilateralExitDelay.Value -= c.PublicUnilateralExitDelay.Value % domain.MinAllowedSequence log.Infof( "public unilateral exit delay must be a multiple of %d, rounded to %d", - minAllowedSequence, c.PublicUnilateralExitDelay.Value, + domain.MinAllowedSequence, c.PublicUnilateralExitDelay.Value, ) } } if c.BoardingExitDelay.Type == arklib.LocktimeTypeSecond { - if c.BoardingExitDelay.Value%minAllowedSequence != 0 { - c.BoardingExitDelay.Value -= c.BoardingExitDelay.Value % minAllowedSequence + if c.BoardingExitDelay.Value%domain.MinAllowedSequence != 0 { + c.BoardingExitDelay.Value -= c.BoardingExitDelay.Value % domain.MinAllowedSequence log.Infof( "boarding exit delay must be a multiple of %d, rounded to %d", - minAllowedSequence, c.BoardingExitDelay, + domain.MinAllowedSequence, c.BoardingExitDelay, ) } } @@ -668,9 +765,6 @@ func (c *Config) Validate() error { return fmt.Errorf("max concurrent streams must be greater than 0") } - if err := c.repoManager(); err != nil { - return err - } if err := c.feeManager(); err != nil { return err } @@ -1021,7 +1115,7 @@ func (c *Config) appService() error { func (c *Config) adminService() error { unit := ports.UnixTime - if c.VtxoTreeExpiry.Value < minAllowedSequence { + if c.VtxoTreeExpiry.Value < domain.MinAllowedSequence { unit = ports.BlockHeight } @@ -1035,6 +1129,14 @@ func (c *Config) adminService() error { c.adminSvc = application.NewAdminService( c.wallet, c.repo, c.txBuilder, c.liveStore, unit, c.fee, c.RoundMinParticipantsCount, c.RoundMaxParticipantsCount, + *c.defaultSettings(), + func(ctx context.Context, settings domain.Settings) error { + // Propagate settings to the running app service. + if c.svc != nil { + return c.svc.UpdateSettings(settings) + } + return nil + }, onInfoChange, ) return nil diff --git a/internal/core/application/admin.go b/internal/core/application/admin.go index 604b12516..5b5da2e7f 100644 --- a/internal/core/application/admin.go +++ b/internal/core/application/admin.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "sync" "time" "github.com/arkade-os/arkd/internal/core/domain" @@ -49,6 +50,9 @@ type AdminService interface { ) (string, string, error) GetExpiringLiquidity(ctx context.Context, after, before int64) (uint64, error) GetRecoverableLiquidity(ctx context.Context) (uint64, error) + GetSettings(ctx context.Context) (*domain.Settings, error) + UpdateSettings(ctx context.Context, settings domain.Settings, updateFields []string) error + ClearSettings(ctx context.Context) error } type adminService struct { @@ -62,13 +66,18 @@ type adminService struct { roundMinParticipantsCount int64 roundMaxParticipantsCount int64 - onInfoChange func() + settingsMu sync.Mutex + defaultSettings domain.Settings + onSettingsUpdated func(context.Context, domain.Settings) error + onInfoChange func() } func NewAdminService( walletSvc ports.WalletService, repoManager ports.RepoManager, txBuilder ports.TxBuilder, liveStoreSvc ports.LiveStore, timeUnit ports.TimeUnit, feeManager ports.FeeManager, roundMinParticipantsCount, roundMaxParticipantsCount int64, + defaultSettings domain.Settings, + onSettingsUpdated func(context.Context, domain.Settings) error, onInfoChange func(), ) AdminService { return &adminService{ @@ -80,6 +89,8 @@ func NewAdminService( feeManager: feeManager, roundMinParticipantsCount: roundMinParticipantsCount, roundMaxParticipantsCount: roundMaxParticipantsCount, + defaultSettings: defaultSettings, + onSettingsUpdated: onSettingsUpdated, onInfoChange: onInfoChange, } } @@ -607,6 +618,84 @@ func (a *adminService) GetRecoverableLiquidity(ctx context.Context) (uint64, err return a.repoManager.Vtxos().GetRecoverableLiquidity(ctx) } +func (a *adminService) GetSettings(ctx context.Context) (*domain.Settings, error) { + return a.repoManager.Settings().Get(ctx) +} + +func (a *adminService) UpdateSettings( + ctx context.Context, + settings domain.Settings, + updateFields []string, +) error { + a.settingsMu.Lock() + defer a.settingsMu.Unlock() + + // Merge the request with stored settings. When updateFields is provided, + // only the listed fields are written from the request; the rest stay as + // they were. When updateFields is empty, every field from the request is + // written as-is. Fields not set in the request default to 0 so callers + // must populate all fields. + current, err := a.repoManager.Settings().Get(ctx) + if err != nil { + return fmt.Errorf("failed to get current settings: %w", err) + } + // nil means no settings exist yet (first boot) so then skip merge + // so caller's full settings are used as-is. + if current != nil { + var mergeErr error + settings, mergeErr = settings.Merge(*current, updateFields) + if mergeErr != nil { + return mergeErr + } + } + + if err := settings.Validate(); err != nil { + return err + } + + // Apply to the running service before persisting so that if live-apply + // fails we don't leave invalid settings in the DB. + if a.onSettingsUpdated != nil { + if err := a.onSettingsUpdated(ctx, settings); err != nil { + return err + } + } + + settings.UpdatedAt = time.Now() + if err := a.repoManager.Settings().Upsert(ctx, settings); err != nil { + return err + } + + a.roundMinParticipantsCount = settings.RoundMinParticipantsCount + a.roundMaxParticipantsCount = settings.RoundMaxParticipantsCount + a.onInfoChange() + return nil +} + +func (a *adminService) ClearSettings(ctx context.Context) error { + a.settingsMu.Lock() + defer a.settingsMu.Unlock() + + defaults := a.defaultSettings + + // Apply to the running service before persisting so that if live-apply + // fails we don't leave inconsistent state in the DB. + if a.onSettingsUpdated != nil { + if err := a.onSettingsUpdated(ctx, defaults); err != nil { + return err + } + } + + defaults.UpdatedAt = time.Now() + if err := a.repoManager.Settings().Upsert(ctx, defaults); err != nil { + return err + } + a.roundMinParticipantsCount = defaults.RoundMinParticipantsCount + a.roundMaxParticipantsCount = defaults.RoundMaxParticipantsCount + a.onInfoChange() + return nil +} + func (a *adminService) getScheduledSweep( ctx context.Context, commitmentTxid string, ) (*ScheduledSweep, error) { diff --git a/internal/core/application/admin_test.go b/internal/core/application/admin_test.go new file mode 100644 index 000000000..4a2cf8141 --- /dev/null +++ b/internal/core/application/admin_test.go @@ -0,0 +1,220 @@ +package application_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/arkade-os/arkd/internal/core/application" + "github.com/arkade-os/arkd/internal/core/domain" + "github.com/arkade-os/arkd/internal/core/ports" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAdminService_Settings(t *testing.T) { + repo := &mockRepoManager{ + settingsRepo: &mockSettingsRepository{}, + } + defaults := domain.Settings{ + BanThreshold: 3, + BanDuration: 300, + VtxoTreeExpiry: 604672, + UnilateralExitDelay: 86400, + PublicUnilateralExitDelay: 86400, + CheckpointExitDelay: 86400, + BoardingExitDelay: 7776000, + RoundMinParticipantsCount: 1, + RoundMaxParticipantsCount: 128, + UtxoMaxAmount: -1, + UtxoMinAmount: -1, + VtxoMaxAmount: -1, + VtxoMinAmount: -1, + MaxTxWeight: 40000, + } + svc := application.NewAdminService( + nil, repo, nil, nil, ports.UnixTime, nil, 1, 128, + defaults, nil, func() {}, + ) + + ctx := context.Background() + + t.Run("get returns nil when no settings exist", func(t *testing.T) { + settings, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.Nil(t, settings) + }) + + t.Run("update persists settings and sets UpdatedAt", func(t *testing.T) { + before := time.Now().Add(-time.Second) + input := domain.Settings{ + BanThreshold: 3, + BanDuration: 300, + VtxoTreeExpiry: 604672, + UnilateralExitDelay: 86400, + PublicUnilateralExitDelay: 86400, + CheckpointExitDelay: 86400, + BoardingExitDelay: 7776000, + RoundMinParticipantsCount: 1, + RoundMaxParticipantsCount: 128, + UtxoMaxAmount: -1, + UtxoMinAmount: -1, + VtxoMaxAmount: -1, + VtxoMinAmount: -1, + MaxTxWeight: 40000, + } + err := svc.UpdateSettings(ctx, input, nil) + require.NoError(t, err) + + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, input.BanThreshold, got.BanThreshold) + assert.Equal(t, input.BanDuration, got.BanDuration) + assert.Equal(t, input.VtxoTreeExpiry, got.VtxoTreeExpiry) + assert.Equal(t, input.UnilateralExitDelay, got.UnilateralExitDelay) + assert.Equal(t, input.PublicUnilateralExitDelay, got.PublicUnilateralExitDelay) + assert.Equal(t, input.CheckpointExitDelay, got.CheckpointExitDelay) + assert.Equal(t, input.BoardingExitDelay, got.BoardingExitDelay) + assert.Equal(t, input.RoundMinParticipantsCount, got.RoundMinParticipantsCount) + assert.Equal(t, input.RoundMaxParticipantsCount, got.RoundMaxParticipantsCount) + assert.Equal(t, input.UtxoMaxAmount, got.UtxoMaxAmount) + assert.Equal(t, input.UtxoMinAmount, got.UtxoMinAmount) + assert.Equal(t, input.VtxoMaxAmount, got.VtxoMaxAmount) + assert.Equal(t, input.VtxoMinAmount, got.VtxoMinAmount) + assert.Equal(t, input.MaxTxWeight, got.MaxTxWeight) + assert.Equal(t, input.SettlementMinExpiryGap, got.SettlementMinExpiryGap) + assert.Equal(t, input.VtxoNoCsvValidationCutoffDate, got.VtxoNoCsvValidationCutoffDate) + + // UpdatedAt must have been set automatically + assert.True(t, got.UpdatedAt.After(before), "UpdatedAt should be set by UpdateSettings") + }) + + t.Run("get returns previously stored settings", func(t *testing.T) { + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, int64(3), got.BanThreshold) + assert.Equal(t, int64(128), got.RoundMaxParticipantsCount) + }) + + t.Run("update overwrites existing settings", func(t *testing.T) { + updated := domain.Settings{ + BanThreshold: 5, + BanDuration: 600, + VtxoTreeExpiry: 604672, + UnilateralExitDelay: 86400, + PublicUnilateralExitDelay: 86400, + CheckpointExitDelay: 86400, + BoardingExitDelay: 7776000, + RoundMinParticipantsCount: 2, + RoundMaxParticipantsCount: 256, + UtxoMaxAmount: 1000000, + UtxoMinAmount: 1000, + VtxoMaxAmount: 500000, + VtxoMinAmount: 500, + MaxTxWeight: 80000, + SettlementMinExpiryGap: 7200, + } + err := svc.UpdateSettings(ctx, updated, nil) + require.NoError(t, err) + + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, int64(5), got.BanThreshold) + assert.Equal(t, int64(600), got.BanDuration) + assert.Equal(t, int64(256), got.RoundMaxParticipantsCount) + assert.Equal(t, int64(1000000), got.UtxoMaxAmount) + assert.Equal(t, int64(80000), got.MaxTxWeight) + assert.Equal(t, int64(7200), got.SettlementMinExpiryGap) + }) + + t.Run("partial update only changes provided fields", func(t *testing.T) { + // Get current state after the full update above. + before, err := svc.GetSettings(ctx) + require.NoError(t, err) + + // Send only BanThreshold via update mask. + partial := domain.Settings{BanThreshold: 99} + err = svc.UpdateSettings(ctx, partial, []string{"ban_threshold"}) + require.NoError(t, err) + + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, int64(99), got.BanThreshold) + // Other fields unchanged. + assert.Equal(t, before.BanDuration, got.BanDuration) + assert.Equal(t, before.UnilateralExitDelay, got.UnilateralExitDelay) + assert.Equal(t, before.BoardingExitDelay, got.BoardingExitDelay) + assert.Equal(t, before.VtxoTreeExpiry, got.VtxoTreeExpiry) + assert.Equal(t, before.MaxTxWeight, got.MaxTxWeight) + }) + + t.Run("clear resets settings to defaults", func(t *testing.T) { + err := svc.ClearSettings(ctx) + require.NoError(t, err) + + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, defaults.BanThreshold, got.BanThreshold) + assert.Equal(t, defaults.BanDuration, got.BanDuration) + assert.Equal(t, defaults.RoundMaxParticipantsCount, got.RoundMaxParticipantsCount) + }) + + t.Run("clear on defaults is idempotent", func(t *testing.T) { + err := svc.ClearSettings(ctx) + require.NoError(t, err) + + got, err := svc.GetSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, defaults.BanThreshold, got.BanThreshold) + }) +} + +// Minimal mock implementations for testing settings methods only. + +type mockRepoManager struct { + ports.RepoManager + settingsRepo domain.SettingsRepository +} + +func (m *mockRepoManager) Settings() domain.SettingsRepository { + return m.settingsRepo +} + +type mockSettingsRepository struct { + mu sync.Mutex + settings *domain.Settings +} + +func (m *mockSettingsRepository) Get(_ context.Context) (*domain.Settings, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.settings == nil { + return nil, nil + } + cp := *m.settings + return &cp, nil +} + +func (m *mockSettingsRepository) Upsert(_ context.Context, settings domain.Settings) error { + m.mu.Lock() + defer m.mu.Unlock() + m.settings = &settings + return nil +} + +func (m *mockSettingsRepository) Clear(_ context.Context) error { + m.mu.Lock() + defer m.mu.Unlock() + m.settings = nil + return nil +} + +func (m *mockSettingsRepository) Close() {} diff --git a/internal/core/application/service.go b/internal/core/application/service.go index 7bb8b29a3..d5af5ed61 100644 --- a/internal/core/application/service.go +++ b/internal/core/application/service.go @@ -299,6 +299,93 @@ func (s *service) Start() error { return nil } +// UpdateSettings applies new settings to the running service. It is called by +// the admin service under settingsMu and must not be called concurrently. +func (s *service) UpdateSettings(settings domain.Settings) error { + // Warn when exit delay parameters change — existing VTXOs have the old + // values baked into their scripts, so changes only affect future rounds. + newUnilateral := domain.ToRelativeLocktime(settings.UnilateralExitDelay) + newPublicUnilateral := domain.ToRelativeLocktime(settings.PublicUnilateralExitDelay) + newCheckpoint := domain.ToRelativeLocktime(settings.CheckpointExitDelay) + newBoarding := domain.ToRelativeLocktime(settings.BoardingExitDelay) + newExpiry := domain.ToRelativeLocktime(settings.VtxoTreeExpiry) + if s.unilateralExitDelay != newUnilateral { + log.Warnf( + "unilateral exit delay changed from %d to %d — only affects future rounds", + s.unilateralExitDelay.Value, newUnilateral.Value, + ) + } + if s.publicUnilateralExitDelay != newPublicUnilateral { + log.Warnf( + "public unilateral exit delay changed from %d to %d — only affects future rounds", + s.publicUnilateralExitDelay.Value, newPublicUnilateral.Value, + ) + } + if s.checkpointExitDelay != newCheckpoint { + log.Warnf( + "checkpoint exit delay changed from %d to %d — only affects future rounds", + s.checkpointExitDelay.Value, newCheckpoint.Value, + ) + } + if s.boardingExitDelay != newBoarding { + log.Warnf( + "boarding exit delay changed from %d to %d — only affects future rounds", + s.boardingExitDelay.Value, newBoarding.Value, + ) + } + if s.batchExpiry != newExpiry { + log.Warnf( + "vtxo tree expiry changed from %d to %d — only affects future rounds", + s.batchExpiry.Value, newExpiry.Value, + ) + } + + s.banDuration = time.Duration(settings.BanDuration) * time.Second + s.banThreshold = settings.BanThreshold + s.unilateralExitDelay = newUnilateral + s.publicUnilateralExitDelay = newPublicUnilateral + s.checkpointExitDelay = newCheckpoint + s.boardingExitDelay = newBoarding + s.batchExpiry = newExpiry + s.roundMinParticipantsCount = settings.RoundMinParticipantsCount + s.roundMaxParticipantsCount = settings.RoundMaxParticipantsCount + s.vtxoMinAmount = settings.VtxoMinAmount + s.vtxoMaxAmount = settings.VtxoMaxAmount + s.utxoMinAmount = settings.UtxoMinAmount + s.utxoMaxAmount = settings.UtxoMaxAmount + s.settlementMinExpiryGap = time.Duration(settings.SettlementMinExpiryGap) * time.Second + s.vtxoNoCsvValidationCutoffTime = time.Unix(settings.VtxoNoCsvValidationCutoffDate, 0) + s.maxTxWeight = uint64(settings.MaxTxWeight) + + // Recalculate dust-derived values. + ctx := context.Background() + dustAmount, err := s.wallet.GetDustAmount(ctx) + if err != nil { + return fmt.Errorf("failed to get dust amount: %s", err) + } + + s.vtxoMinAmount, s.utxoMinAmount = resolveMinAmounts( + s.vtxoMinAmount, s.utxoMinAmount, int64(dustAmount), + ) + + // Recalculate checkpoint tapscript if forfeit pubkey is already set. + if s.forfeitPubkey != nil { + checkpointClosure := &script.CSVMultisigClosure{ + Locktime: s.checkpointExitDelay, + MultisigClosure: script.MultisigClosure{ + PubKeys: []*btcec.PublicKey{s.forfeitPubkey}, + }, + } + checkpointTapscript, err := checkpointClosure.Script() + if err != nil { + return fmt.Errorf("failed to encode checkpoint tapscript: %s", err) + } + s.checkpointTapscript = checkpointTapscript + } + + return nil +} + func (s *service) registerEventHandlers() { s.repoManager.RegisterBatchUpdateHandler( func(round domain.Round) { diff --git a/internal/core/application/types.go b/internal/core/application/types.go index 561bfcff7..cb70d4e65 100644 --- a/internal/core/application/types.go +++ b/internal/core/application/types.go @@ -67,6 +67,7 @@ type Service interface { proof intent.Proof, message intent.GetIntentMessage, ) ([]*domain.Intent, errors.Error) + UpdateSettings(settings domain.Settings) error RefreshInfoCache() } diff --git a/internal/core/domain/settings.go b/internal/core/domain/settings.go new file mode 100644 index 000000000..0b9e4861d --- /dev/null +++ b/internal/core/domain/settings.go @@ -0,0 +1,272 @@ +package domain + +import ( + "fmt" + "math" + "reflect" + "strings" + "time" + "unicode" + + arklib "github.com/arkade-os/arkd/pkg/ark-lib" +) + +const ( + MinAllowedSequence = 512 + // MaxSatoshis is the maximum number of satoshis that can ever exist (21M BTC). + MaxSatoshis = 21_000_000 * 1e8 +) + +// ErrInvalidSettings is returned when settings fail validation. +type ErrInvalidSettings struct { + Reason string +} + +func (e *ErrInvalidSettings) Error() string { + return fmt.Sprintf("invalid settings: %s", e.Reason) +} + +// ToRelativeLocktime converts a raw locktime value to a typed RelativeLocktime. +// Values >= MinAllowedSequence (512) are interpreted as seconds (BIP68 +// time-based relative locktimes use 512-second granularity units), while values +// < 512 are interpreted as blocks. Config validation enforces that time-based +// values are multiples of 512. +func ToRelativeLocktime(locktime int64) arklib.RelativeLocktime { + if locktime >= MinAllowedSequence { + return arklib.RelativeLocktime{Type: arklib.LocktimeTypeSecond, Value: uint32(locktime)} + } + return arklib.RelativeLocktime{Type: arklib.LocktimeTypeBlock, Value: uint32(locktime)} +} + +type Settings struct { + BanThreshold int64 + BanDuration int64 + UnilateralExitDelay int64 + PublicUnilateralExitDelay int64 + CheckpointExitDelay int64 + BoardingExitDelay int64 + VtxoTreeExpiry int64 + RoundMinParticipantsCount int64 + RoundMaxParticipantsCount int64 + VtxoMinAmount int64 + VtxoMaxAmount int64 + UtxoMinAmount int64 + UtxoMaxAmount int64 + SettlementMinExpiryGap int64 + VtxoNoCsvValidationCutoffDate int64 + MaxTxWeight int64 + UpdatedAt time.Time +} + +func (s Settings) Validate() error { + if s.UnilateralExitDelay <= 0 { + return &ErrInvalidSettings{"unilateral exit delay must be greater than 0"} + } + if s.BoardingExitDelay <= 0 { + return &ErrInvalidSettings{"boarding exit delay must be greater than 0"} + } + if s.VtxoTreeExpiry <= 0 { + return &ErrInvalidSettings{"vtxo tree expiry must be greater than 0"} + } + if s.BanThreshold < 1 { + return &ErrInvalidSettings{"ban threshold must be at least 1"} + } + if s.BanDuration < 1 { + return &ErrInvalidSettings{"ban duration must be at least 1"} + } + if s.RoundMinParticipantsCount < 1 { + return &ErrInvalidSettings{"round min participants count must be at least 1"} + } + if s.RoundMaxParticipantsCount < s.RoundMinParticipantsCount { + return &ErrInvalidSettings{ + "round max participants count must be >= round min participants count", + } + } + if s.PublicUnilateralExitDelay < s.UnilateralExitDelay { + return &ErrInvalidSettings{ + "public unilateral exit delay must be >= unilateral exit delay", + } + } + if s.UnilateralExitDelay == s.BoardingExitDelay { + return &ErrInvalidSettings{ + "unilateral exit delay and boarding exit delay must be different", + } + } + if s.CheckpointExitDelay <= 0 { + return &ErrInvalidSettings{"checkpoint exit delay must be greater than 0"} + } + if s.MaxTxWeight <= 0 { + return &ErrInvalidSettings{"max tx weight must be greater than 0"} + } + if s.UnilateralExitDelay > math.MaxUint32 { + return &ErrInvalidSettings{"unilateral exit delay exceeds maximum uint32 value"} + } + if s.BoardingExitDelay > math.MaxUint32 { + return &ErrInvalidSettings{"boarding exit delay exceeds maximum uint32 value"} + } + if s.VtxoTreeExpiry > math.MaxUint32 { + return &ErrInvalidSettings{"vtxo tree expiry exceeds maximum uint32 value"} + } + if s.CheckpointExitDelay > math.MaxUint32 { + return &ErrInvalidSettings{"checkpoint exit delay exceeds maximum uint32 value"} + } + if s.VtxoMinAmount < -1 || s.VtxoMinAmount > MaxSatoshis { + return &ErrInvalidSettings{"vtxo min amount must be -1 (dust) or between 0 and 21M BTC"} + } + if s.VtxoMaxAmount < -1 || s.VtxoMaxAmount > MaxSatoshis { + return &ErrInvalidSettings{"vtxo max amount must be -1 (unset) or between 0 and 21M BTC"} + } + if s.UtxoMinAmount < -1 || s.UtxoMinAmount > MaxSatoshis { + return &ErrInvalidSettings{"utxo min amount must be -1 (dust) or between 0 and 21M BTC"} + } + if s.UtxoMaxAmount < -1 || s.UtxoMaxAmount > MaxSatoshis { + return &ErrInvalidSettings{"utxo max amount must be -1 (unset) or between 0 and 21M BTC"} + } + if s.VtxoMinAmount != -1 && s.VtxoMaxAmount != -1 && s.VtxoMinAmount > s.VtxoMaxAmount { + return &ErrInvalidSettings{"vtxo min amount must be <= vtxo max amount"} + } + if s.UtxoMinAmount != -1 && s.UtxoMaxAmount != -1 && s.UtxoMinAmount > s.UtxoMaxAmount { + return &ErrInvalidSettings{"utxo min amount must be <= utxo max amount"} + } + return nil +} + +// validUpdateFields is the set of snake_case field names accepted by Merge, +// built from the Settings struct fields (excluding UpdatedAt). +var validUpdateFields = buildValidUpdateFields() + +func buildValidUpdateFields() map[string]struct{} { + t := reflect.TypeOf(Settings{}) + fields := make(map[string]struct{}, t.NumField()) + for i := 0; i < t.NumField(); i++ { + name := t.Field(i).Name + if name == "UpdatedAt" { + continue + } + fields[camelToSnake(name)] = struct{}{} + } + return fields +} + +func camelToSnake(s string) string { + var b strings.Builder + for i, r := range s { + if unicode.IsUpper(r) { + if i > 0 { + b.WriteByte('_') + } + b.WriteRune(unicode.ToLower(r)) + } else { + b.WriteRune(r) + } + } + return b.String() +} + +// Merge combines the receiver (incoming request) with stored settings +// according to updateFields. If non-empty, only the listed +// fields are written from the request; all other fields remain as stored. +// If updateFields is empty, every field from the request is written as-is — +// fields not set in the request default to 0, so callers must populate all +// fields. Field names use snake_case matching the proto field names. +// Returns an error if any field name in updateFields is not recognized. +func (s Settings) Merge(stored Settings, updateFields []string) (Settings, error) { + if len(updateFields) == 0 { + s.UpdatedAt = stored.UpdatedAt + return s, nil + } + + fields := make(map[string]struct{}, len(updateFields)) + for _, f := range updateFields { + if _, ok := validUpdateFields[f]; !ok { + return Settings{}, fmt.Errorf("unknown update field: %q", f) + } + if _, dup := fields[f]; dup { + return Settings{}, fmt.Errorf("duplicate update field: %q", f) + } + fields[f] = struct{}{} + } + + result := stored + if _, ok := fields["ban_threshold"]; ok { + result.BanThreshold = s.BanThreshold + } + if _, ok := fields["ban_duration"]; ok { + result.BanDuration = s.BanDuration + } + if _, ok := fields["unilateral_exit_delay"]; ok { + result.UnilateralExitDelay = s.UnilateralExitDelay + } + if _, ok := fields["public_unilateral_exit_delay"]; ok { + result.PublicUnilateralExitDelay = s.PublicUnilateralExitDelay + } + if _, ok := fields["checkpoint_exit_delay"]; ok { + result.CheckpointExitDelay = s.CheckpointExitDelay + } + if _, ok := fields["boarding_exit_delay"]; ok { + result.BoardingExitDelay = s.BoardingExitDelay + } + if _, ok := fields["vtxo_tree_expiry"]; ok { + result.VtxoTreeExpiry = s.VtxoTreeExpiry + } + if _, ok := fields["round_min_participants_count"]; ok { + result.RoundMinParticipantsCount = s.RoundMinParticipantsCount + } + if _, ok := fields["round_max_participants_count"]; ok { + result.RoundMaxParticipantsCount = s.RoundMaxParticipantsCount + } + if _, ok := fields["vtxo_min_amount"]; ok { + result.VtxoMinAmount = s.VtxoMinAmount + } + if _, ok := fields["vtxo_max_amount"]; ok { + result.VtxoMaxAmount = s.VtxoMaxAmount + } + if _, ok := fields["utxo_min_amount"]; ok { + result.UtxoMinAmount = s.UtxoMinAmount + } + if _, ok := fields["utxo_max_amount"]; ok { + result.UtxoMaxAmount = s.UtxoMaxAmount + } + if _, ok := fields["settlement_min_expiry_gap"]; ok { + result.SettlementMinExpiryGap = s.SettlementMinExpiryGap + } + if _, ok := fields["vtxo_no_csv_validation_cutoff_date"]; ok { + result.VtxoNoCsvValidationCutoffDate = s.VtxoNoCsvValidationCutoffDate + } + if _, ok := fields["max_tx_weight"]; ok { + result.MaxTxWeight = s.MaxTxWeight + } + return result, nil +} + +func NewSettings( + banThreshold, banDuration, + unilateralExitDelay, publicUnilateralExitDelay, + checkpointExitDelay, boardingExitDelay, + vtxoTreeExpiry, + roundMinParticipantsCount, roundMaxParticipantsCount, + vtxoMinAmount, vtxoMaxAmount, + utxoMinAmount, utxoMaxAmount, + settlementMinExpiryGap, + vtxoNoCsvValidationCutoffDate, + maxTxWeight int64, +) *Settings { + return &Settings{ + BanThreshold: banThreshold, + BanDuration: banDuration, + UnilateralExitDelay: unilateralExitDelay, + PublicUnilateralExitDelay: publicUnilateralExitDelay, + CheckpointExitDelay: checkpointExitDelay, + BoardingExitDelay: boardingExitDelay, + VtxoTreeExpiry: vtxoTreeExpiry, + RoundMinParticipantsCount: roundMinParticipantsCount, + RoundMaxParticipantsCount: roundMaxParticipantsCount, + VtxoMinAmount: vtxoMinAmount, + VtxoMaxAmount: vtxoMaxAmount, + UtxoMinAmount: utxoMinAmount, + UtxoMaxAmount: utxoMaxAmount, + SettlementMinExpiryGap: settlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: vtxoNoCsvValidationCutoffDate, + MaxTxWeight: maxTxWeight, + } +} diff --git a/internal/core/domain/settings_repo.go b/internal/core/domain/settings_repo.go new file mode 100644 index 000000000..d7923d4a0 --- /dev/null +++ b/internal/core/domain/settings_repo.go @@ -0,0 +1,10 @@ +package domain + +import "context" + +type SettingsRepository interface { + Get(ctx context.Context) (*Settings, error) + Upsert(ctx context.Context, settings Settings) error + Clear(ctx context.Context) error + Close() +} diff --git a/internal/core/domain/settings_test.go b/internal/core/domain/settings_test.go new file mode 100644 index 000000000..e74b37046 --- /dev/null +++ b/internal/core/domain/settings_test.go @@ -0,0 +1,247 @@ +package domain + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func validSettings() Settings { + return Settings{ + BanThreshold: 3, + BanDuration: 300, + VtxoTreeExpiry: 604672, + UnilateralExitDelay: 86400, + PublicUnilateralExitDelay: 86400, + CheckpointExitDelay: 86400, + BoardingExitDelay: 7776000, + RoundMinParticipantsCount: 1, + RoundMaxParticipantsCount: 128, + VtxoMinAmount: -1, + VtxoMaxAmount: -1, + UtxoMinAmount: -1, + UtxoMaxAmount: -1, + MaxTxWeight: 40000, + } +} + +func TestSettings_Validate(t *testing.T) { + t.Run("valid settings pass", func(t *testing.T) { + require.NoError(t, validSettings().Validate()) + }) + + t.Run("ban threshold must be at least 1", func(t *testing.T) { + s := validSettings() + s.BanThreshold = 0 + err := s.Validate() + require.Error(t, err) + var validationErr *ErrInvalidSettings + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, validationErr.Reason, "ban threshold") + }) + + t.Run("checkpoint exit delay must be > 0", func(t *testing.T) { + s := validSettings() + s.CheckpointExitDelay = 0 + err := s.Validate() + require.Error(t, err) + var validationErr *ErrInvalidSettings + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, validationErr.Reason, "checkpoint exit delay") + }) + + t.Run("amount lower bound", func(t *testing.T) { + tests := []struct { + name string + field string + set func(*Settings, int64) + }{ + {"vtxo min", "vtxo min amount", func(s *Settings, v int64) { s.VtxoMinAmount = v }}, + {"vtxo max", "vtxo max amount", func(s *Settings, v int64) { s.VtxoMaxAmount = v }}, + {"utxo min", "utxo min amount", func(s *Settings, v int64) { s.UtxoMinAmount = v }}, + {"utxo max", "utxo max amount", func(s *Settings, v int64) { s.UtxoMaxAmount = v }}, + } + for _, tt := range tests { + t.Run(tt.name+" rejects -2", func(t *testing.T) { + s := validSettings() + tt.set(&s, -2) + err := s.Validate() + require.Error(t, err) + var validationErr *ErrInvalidSettings + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, validationErr.Reason, tt.field) + }) + + t.Run(tt.name+" accepts -1", func(t *testing.T) { + s := validSettings() + tt.set(&s, -1) + require.NoError(t, s.Validate()) + }) + } + }) + + t.Run("amount upper bound", func(t *testing.T) { + tests := []struct { + name string + field string + set func(*Settings, int64) + }{ + {"vtxo min", "vtxo min amount", func(s *Settings, v int64) { s.VtxoMinAmount = v }}, + {"vtxo max", "vtxo max amount", func(s *Settings, v int64) { s.VtxoMaxAmount = v }}, + {"utxo min", "utxo min amount", func(s *Settings, v int64) { s.UtxoMinAmount = v }}, + {"utxo max", "utxo max amount", func(s *Settings, v int64) { s.UtxoMaxAmount = v }}, + } + for _, tt := range tests { + t.Run(tt.name+" rejects above max satoshis", func(t *testing.T) { + s := validSettings() + tt.set(&s, MaxSatoshis+1) + err := s.Validate() + require.Error(t, err) + var validationErr *ErrInvalidSettings + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, validationErr.Reason, tt.field) + }) + + t.Run(tt.name+" accepts max satoshis", func(t *testing.T) { + s := validSettings() + tt.set(&s, MaxSatoshis) + require.NoError(t, s.Validate()) + }) + } + }) + + t.Run("uint32 overflow", func(t *testing.T) { + tests := []struct { + name string + set func(*Settings) + }{ + {"unilateral exit delay", func(s *Settings) { + s.UnilateralExitDelay = math.MaxUint32 + 1 + s.PublicUnilateralExitDelay = math.MaxUint32 + 1 + }}, + {"boarding exit delay", func(s *Settings) { + s.BoardingExitDelay = math.MaxUint32 + 1 + }}, + {"vtxo tree expiry", func(s *Settings) { + s.VtxoTreeExpiry = math.MaxUint32 + 1 + }}, + {"checkpoint exit delay", func(s *Settings) { + s.CheckpointExitDelay = math.MaxUint32 + 1 + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := validSettings() + tt.set(&s) + err := s.Validate() + require.Error(t, err) + var validationErr *ErrInvalidSettings + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, validationErr.Reason, "exceeds maximum uint32") + }) + } + }) + + t.Run("merge with update_fields updates only listed fields", func(t *testing.T) { + current := validSettings() + partial := Settings{BanThreshold: 10} + merged, err := partial.Merge(current, []string{"ban_threshold"}) + require.NoError(t, err) + + assert.Equal(t, int64(10), merged.BanThreshold) + assert.Equal(t, current.BanDuration, merged.BanDuration) + assert.Equal(t, current.UnilateralExitDelay, merged.UnilateralExitDelay) + assert.Equal(t, current.BoardingExitDelay, merged.BoardingExitDelay) + assert.Equal(t, current.VtxoTreeExpiry, merged.VtxoTreeExpiry) + assert.Equal(t, current.MaxTxWeight, merged.MaxTxWeight) + require.NoError(t, merged.Validate()) + }) + + t.Run("merge with update_fields allows setting field to zero", func(t *testing.T) { + current := validSettings() + current.SettlementMinExpiryGap = 3600 + update := Settings{SettlementMinExpiryGap: 0} + merged, err := update.Merge(current, []string{"settlement_min_expiry_gap"}) + require.NoError(t, err) + + assert.Equal(t, int64(0), merged.SettlementMinExpiryGap) + // Other fields unchanged. + assert.Equal(t, current.BanThreshold, merged.BanThreshold) + }) + + t.Run("merge with empty update_fields replaces all fields", func(t *testing.T) { + current := validSettings() + full := validSettings() + full.BanThreshold = 99 + merged, err := full.Merge(current, nil) + require.NoError(t, err) + + assert.Equal(t, int64(99), merged.BanThreshold) + assert.Equal(t, full.BanDuration, merged.BanDuration) + }) + + t.Run("merge rejects unknown update_fields", func(t *testing.T) { + current := validSettings() + _, err := current.Merge(current, []string{"ban_threshol"}) + require.Error(t, err) + assert.Contains(t, err.Error(), `unknown update field: "ban_threshol"`) + }) + + t.Run("merge rejects duplicate update_fields", func(t *testing.T) { + current := validSettings() + _, err := current.Merge(current, []string{"ban_threshold", "ban_threshold"}) + require.Error(t, err) + assert.Contains(t, err.Error(), `duplicate update field: "ban_threshold"`) + }) + + t.Run("validUpdateFields matches Settings struct", func(t *testing.T) { + expected := map[string]struct{}{ + "ban_threshold": {}, + "ban_duration": {}, + "unilateral_exit_delay": {}, + "public_unilateral_exit_delay": {}, + "checkpoint_exit_delay": {}, + "boarding_exit_delay": {}, + "vtxo_tree_expiry": {}, + "round_min_participants_count": {}, + "round_max_participants_count": {}, + "vtxo_min_amount": {}, + "vtxo_max_amount": {}, + "utxo_min_amount": {}, + "utxo_max_amount": {}, + "settlement_min_expiry_gap": {}, + "vtxo_no_csv_validation_cutoff_date": {}, + "max_tx_weight": {}, + } + assert.Equal(t, expected, validUpdateFields) + }) + + t.Run("min exceeds max", func(t *testing.T) { + t.Run("vtxo", func(t *testing.T) { + s := validSettings() + s.VtxoMinAmount = 1000 + s.VtxoMaxAmount = 500 + err := s.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "vtxo min amount must be <= vtxo max amount") + }) + + t.Run("utxo", func(t *testing.T) { + s := validSettings() + s.UtxoMinAmount = 1000 + s.UtxoMaxAmount = 500 + err := s.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "utxo min amount must be <= utxo max amount") + }) + + t.Run("skipped when sentinel", func(t *testing.T) { + s := validSettings() + s.VtxoMinAmount = 1000 + s.VtxoMaxAmount = -1 + require.NoError(t, s.Validate()) + }) + }) +} diff --git a/internal/core/ports/repo_manager.go b/internal/core/ports/repo_manager.go index 653f4c373..4a9333970 100644 --- a/internal/core/ports/repo_manager.go +++ b/internal/core/ports/repo_manager.go @@ -11,6 +11,7 @@ type RepoManager interface { Convictions() domain.ConvictionRepository Assets() domain.AssetRepository Fees() domain.FeeRepository + Settings() domain.SettingsRepository RegisterBatchUpdateHandler(handler func(data domain.Round)) RegisterOffchainTxUpdateHandler(handler func(data domain.OffchainTx)) Close() diff --git a/internal/infrastructure/db/badger/settings_repo.go b/internal/infrastructure/db/badger/settings_repo.go new file mode 100644 index 000000000..160cc99d2 --- /dev/null +++ b/internal/infrastructure/db/badger/settings_repo.go @@ -0,0 +1,99 @@ +package badgerdb + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "time" + + "github.com/arkade-os/arkd/internal/core/domain" + "github.com/dgraph-io/badger/v4" + "github.com/timshannon/badgerhold/v4" +) + +const ( + settingsStoreDir = "settings" + settingsKey = "settings" +) + +type settingsRepository struct { + store *badgerhold.Store +} + +func NewSettingsRepository(config ...interface{}) (domain.SettingsRepository, error) { + if len(config) != 2 { + return nil, fmt.Errorf("invalid config") + } + baseDir, ok := config[0].(string) + if !ok { + return nil, fmt.Errorf("invalid base directory") + } + var logger badger.Logger + if config[1] != nil { + logger, ok = config[1].(badger.Logger) + if !ok { + return nil, fmt.Errorf("invalid logger") + } + } + + var dir string + if len(baseDir) > 0 { + dir = filepath.Join(baseDir, settingsStoreDir) + } + store, err := createDB(dir, logger) + if err != nil { + return nil, fmt.Errorf("failed to open settings store: %s", err) + } + + return &settingsRepository{store}, nil +} + +func (r *settingsRepository) Get(ctx context.Context) (*domain.Settings, error) { + var settings domain.Settings + err := r.store.Get(settingsKey, &settings) + if errors.Is(err, badgerhold.ErrNotFound) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get settings: %w", err) + } + return &settings, nil +} + +func (r *settingsRepository) Upsert( + ctx context.Context, settings domain.Settings, +) error { + if err := r.store.Upsert(settingsKey, &settings); err != nil { + if errors.Is(err, badger.ErrConflict) { + attempts := 1 + for errors.Is(err, badger.ErrConflict) && attempts <= maxRetries { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(100 * time.Millisecond): + } + err = r.store.Upsert(settingsKey, &settings) + attempts++ + } + } + return err + } + return nil +} + +func (r *settingsRepository) Clear(ctx context.Context) error { + var settings domain.Settings + if err := r.store.Delete(settingsKey, &settings); err != nil { + if errors.Is(err, badgerhold.ErrNotFound) { + return nil + } + return err + } + return nil +} + +func (r *settingsRepository) Close() { + // nolint:all + r.store.Close() +} diff --git a/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.down.sql b/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.down.sql new file mode 100644 index 000000000..4596c6a00 --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS settings; diff --git a/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.up.sql b/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.up.sql new file mode 100644 index 000000000..afc764a9b --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260227000000_add_settings.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS settings ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + ban_threshold BIGINT NOT NULL DEFAULT 0, + ban_duration BIGINT NOT NULL DEFAULT 0, + unilateral_exit_delay BIGINT NOT NULL DEFAULT 0, + public_unilateral_exit_delay BIGINT NOT NULL DEFAULT 0, + checkpoint_exit_delay BIGINT NOT NULL DEFAULT 0, + boarding_exit_delay BIGINT NOT NULL DEFAULT 0, + vtxo_tree_expiry BIGINT NOT NULL DEFAULT 0, + round_min_participants_count BIGINT NOT NULL DEFAULT 0, + round_max_participants_count BIGINT NOT NULL DEFAULT 0, + vtxo_min_amount BIGINT NOT NULL DEFAULT 0, + vtxo_max_amount BIGINT NOT NULL DEFAULT 0, + utxo_min_amount BIGINT NOT NULL DEFAULT 0, + utxo_max_amount BIGINT NOT NULL DEFAULT 0, + settlement_min_expiry_gap BIGINT NOT NULL DEFAULT 0, + vtxo_no_csv_validation_cutoff_date BIGINT NOT NULL DEFAULT 0, + max_tx_weight BIGINT NOT NULL DEFAULT 0, + updated_at BIGINT NOT NULL +); diff --git a/internal/infrastructure/db/postgres/settings_repo.go b/internal/infrastructure/db/postgres/settings_repo.go new file mode 100644 index 000000000..c67f7fdfe --- /dev/null +++ b/internal/infrastructure/db/postgres/settings_repo.go @@ -0,0 +1,98 @@ +package pgdb + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/arkade-os/arkd/internal/core/domain" + "github.com/arkade-os/arkd/internal/infrastructure/db/postgres/sqlc/queries" +) + +type settingsRepository struct { + db *sql.DB + querier *queries.Queries +} + +func NewSettingsRepository(config ...interface{}) (domain.SettingsRepository, error) { + if len(config) != 1 { + return nil, fmt.Errorf("invalid config") + } + db, ok := config[0].(*sql.DB) + if !ok { + return nil, fmt.Errorf( + "cannot open settings repository: invalid config, expected db at 0", + ) + } + + return &settingsRepository{ + db: db, + querier: queries.New(db), + }, nil +} + +func (r *settingsRepository) Get(ctx context.Context) (*domain.Settings, error) { + settings, err := r.querier.SelectLatestSettings(ctx) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + + return &domain.Settings{ + BanThreshold: settings.BanThreshold, + BanDuration: settings.BanDuration, + UnilateralExitDelay: settings.UnilateralExitDelay, + PublicUnilateralExitDelay: settings.PublicUnilateralExitDelay, + CheckpointExitDelay: settings.CheckpointExitDelay, + BoardingExitDelay: settings.BoardingExitDelay, + VtxoTreeExpiry: settings.VtxoTreeExpiry, + RoundMinParticipantsCount: settings.RoundMinParticipantsCount, + RoundMaxParticipantsCount: settings.RoundMaxParticipantsCount, + VtxoMinAmount: settings.VtxoMinAmount, + VtxoMaxAmount: settings.VtxoMaxAmount, + UtxoMinAmount: settings.UtxoMinAmount, + UtxoMaxAmount: settings.UtxoMaxAmount, + SettlementMinExpiryGap: settings.SettlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: settings.VtxoNoCsvValidationCutoffDate, + MaxTxWeight: settings.MaxTxWeight, + UpdatedAt: time.Unix(settings.UpdatedAt, 0), + }, nil +} + +// Upsert inserts or updates the singleton settings row. The ID field defaults +// to 0 (Go zero value) which is used as the fixed singleton key via ON CONFLICT(id). +func (r *settingsRepository) Upsert( + ctx context.Context, settings domain.Settings, +) error { + return r.querier.UpsertSettings(ctx, queries.UpsertSettingsParams{ + BanThreshold: settings.BanThreshold, + BanDuration: settings.BanDuration, + UnilateralExitDelay: settings.UnilateralExitDelay, + PublicUnilateralExitDelay: settings.PublicUnilateralExitDelay, + CheckpointExitDelay: settings.CheckpointExitDelay, + BoardingExitDelay: settings.BoardingExitDelay, + VtxoTreeExpiry: settings.VtxoTreeExpiry, + RoundMinParticipantsCount: settings.RoundMinParticipantsCount, + RoundMaxParticipantsCount: settings.RoundMaxParticipantsCount, + VtxoMinAmount: settings.VtxoMinAmount, + VtxoMaxAmount: settings.VtxoMaxAmount, + UtxoMinAmount: settings.UtxoMinAmount, + UtxoMaxAmount: settings.UtxoMaxAmount, + SettlementMinExpiryGap: settings.SettlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: settings.VtxoNoCsvValidationCutoffDate, + MaxTxWeight: settings.MaxTxWeight, + UpdatedAt: settings.UpdatedAt.Unix(), + }) +} + +func (r *settingsRepository) Clear(ctx context.Context) error { + return r.querier.ClearSettings(ctx) +} + +func (r *settingsRepository) Close() { + _ = r.db.Close() +} diff --git a/internal/infrastructure/db/postgres/sqlc/queries/models.go b/internal/infrastructure/db/postgres/sqlc/queries/models.go index d8dd5c036..77c9317e7 100644 --- a/internal/infrastructure/db/postgres/sqlc/queries/models.go +++ b/internal/infrastructure/db/postgres/sqlc/queries/models.go @@ -206,6 +206,27 @@ type ScheduledSession struct { UpdatedAt int64 } +type Setting struct { + ID int64 + BanThreshold int64 + BanDuration int64 + UnilateralExitDelay int64 + PublicUnilateralExitDelay int64 + CheckpointExitDelay int64 + BoardingExitDelay int64 + VtxoTreeExpiry int64 + RoundMinParticipantsCount int64 + RoundMaxParticipantsCount int64 + VtxoMinAmount int64 + VtxoMaxAmount int64 + UtxoMinAmount int64 + UtxoMaxAmount int64 + SettlementMinExpiryGap int64 + VtxoNoCsvValidationCutoffDate int64 + MaxTxWeight int64 + UpdatedAt int64 +} + type Tx struct { Txid string Tx string diff --git a/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go b/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go index a59cab93d..6635dfceb 100644 --- a/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go +++ b/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go @@ -86,6 +86,15 @@ func (q *Queries) ClearScheduledSession(ctx context.Context) error { return err } +const clearSettings = `-- name: ClearSettings :exec +DELETE FROM settings +` + +func (q *Queries) ClearSettings(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, clearSettings) + return err +} + const insertAsset = `-- name: InsertAsset :exec INSERT INTO asset (id, is_immutable, metadata_hash, metadata, control_asset_id) VALUES ($1, $2, $3, $4, $5) @@ -529,6 +538,36 @@ func (q *Queries) SelectLatestScheduledSession(ctx context.Context) (ScheduledSe return i, err } +const selectLatestSettings = `-- name: SelectLatestSettings :one +SELECT id, ban_threshold, ban_duration, unilateral_exit_delay, public_unilateral_exit_delay, checkpoint_exit_delay, boarding_exit_delay, vtxo_tree_expiry, round_min_participants_count, round_max_participants_count, vtxo_min_amount, vtxo_max_amount, utxo_min_amount, utxo_max_amount, settlement_min_expiry_gap, vtxo_no_csv_validation_cutoff_date, max_tx_weight, updated_at FROM settings ORDER BY updated_at DESC LIMIT 1 +` + +func (q *Queries) SelectLatestSettings(ctx context.Context) (Setting, error) { + row := q.db.QueryRowContext(ctx, selectLatestSettings) + var i Setting + err := row.Scan( + &i.ID, + &i.BanThreshold, + &i.BanDuration, + &i.UnilateralExitDelay, + &i.PublicUnilateralExitDelay, + &i.CheckpointExitDelay, + &i.BoardingExitDelay, + &i.VtxoTreeExpiry, + &i.RoundMinParticipantsCount, + &i.RoundMaxParticipantsCount, + &i.VtxoMinAmount, + &i.VtxoMaxAmount, + &i.UtxoMinAmount, + &i.UtxoMaxAmount, + &i.SettlementMinExpiryGap, + &i.VtxoNoCsvValidationCutoffDate, + &i.MaxTxWeight, + &i.UpdatedAt, + ) + return i, err +} + const selectNotUnrolledVtxos = `-- name: SelectNotUnrolledVtxos :many SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.swept, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.commitments, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false ` @@ -2148,6 +2187,95 @@ func (q *Queries) UpsertScheduledSession(ctx context.Context, arg UpsertSchedule return err } +const upsertSettings = `-- name: UpsertSettings :exec +INSERT INTO settings ( + id, ban_threshold, ban_duration, + unilateral_exit_delay, public_unilateral_exit_delay, + checkpoint_exit_delay, boarding_exit_delay, + vtxo_tree_expiry, + round_min_participants_count, round_max_participants_count, + vtxo_min_amount, vtxo_max_amount, + utxo_min_amount, utxo_max_amount, + settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date, + max_tx_weight, updated_at +) VALUES ( + $1, $2, $3, + $4, $5, + $6, $7, + $8, + $9, $10, + $11, $12, + $13, $14, + $15, + $16, + $17, $18 +) +ON CONFLICT(id) DO UPDATE SET + ban_threshold = EXCLUDED.ban_threshold, + ban_duration = EXCLUDED.ban_duration, + unilateral_exit_delay = EXCLUDED.unilateral_exit_delay, + public_unilateral_exit_delay = EXCLUDED.public_unilateral_exit_delay, + checkpoint_exit_delay = EXCLUDED.checkpoint_exit_delay, + boarding_exit_delay = EXCLUDED.boarding_exit_delay, + vtxo_tree_expiry = EXCLUDED.vtxo_tree_expiry, + round_min_participants_count = EXCLUDED.round_min_participants_count, + round_max_participants_count = EXCLUDED.round_max_participants_count, + vtxo_min_amount = EXCLUDED.vtxo_min_amount, + vtxo_max_amount = EXCLUDED.vtxo_max_amount, + utxo_min_amount = EXCLUDED.utxo_min_amount, + utxo_max_amount = EXCLUDED.utxo_max_amount, + settlement_min_expiry_gap = EXCLUDED.settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date = EXCLUDED.vtxo_no_csv_validation_cutoff_date, + max_tx_weight = EXCLUDED.max_tx_weight, + updated_at = EXCLUDED.updated_at +` + +type UpsertSettingsParams struct { + ID int64 + BanThreshold int64 + BanDuration int64 + UnilateralExitDelay int64 + PublicUnilateralExitDelay int64 + CheckpointExitDelay int64 + BoardingExitDelay int64 + VtxoTreeExpiry int64 + RoundMinParticipantsCount int64 + RoundMaxParticipantsCount int64 + VtxoMinAmount int64 + VtxoMaxAmount int64 + UtxoMinAmount int64 + UtxoMaxAmount int64 + SettlementMinExpiryGap int64 + VtxoNoCsvValidationCutoffDate int64 + MaxTxWeight int64 + UpdatedAt int64 +} + +func (q *Queries) UpsertSettings(ctx context.Context, arg UpsertSettingsParams) error { + _, err := q.db.ExecContext(ctx, upsertSettings, + arg.ID, + arg.BanThreshold, + arg.BanDuration, + arg.UnilateralExitDelay, + arg.PublicUnilateralExitDelay, + arg.CheckpointExitDelay, + arg.BoardingExitDelay, + arg.VtxoTreeExpiry, + arg.RoundMinParticipantsCount, + arg.RoundMaxParticipantsCount, + arg.VtxoMinAmount, + arg.VtxoMaxAmount, + arg.UtxoMinAmount, + arg.UtxoMaxAmount, + arg.SettlementMinExpiryGap, + arg.VtxoNoCsvValidationCutoffDate, + arg.MaxTxWeight, + arg.UpdatedAt, + ) + return err +} + const upsertTx = `-- name: UpsertTx :exec INSERT INTO tx (tx, round_id, type, position, txid, children) VALUES ($1, $2, $3, $4, $5, $6) diff --git a/internal/infrastructure/db/postgres/sqlc/query.sql b/internal/infrastructure/db/postgres/sqlc/query.sql index f24c4f701..abef38db3 100644 --- a/internal/infrastructure/db/postgres/sqlc/query.sql +++ b/internal/infrastructure/db/postgres/sqlc/query.sql @@ -451,4 +451,53 @@ WHERE ap.asset_id = $1 AND v.spent = false; SELECT control_asset_id FROM asset WHERE id = $1; -- name: SelectAssetExists :one -SELECT 1 FROM asset WHERE id = $1 LIMIT 1; \ No newline at end of file +SELECT 1 FROM asset WHERE id = $1 LIMIT 1; + +-- name: UpsertSettings :exec +INSERT INTO settings ( + id, ban_threshold, ban_duration, + unilateral_exit_delay, public_unilateral_exit_delay, + checkpoint_exit_delay, boarding_exit_delay, + vtxo_tree_expiry, + round_min_participants_count, round_max_participants_count, + vtxo_min_amount, vtxo_max_amount, + utxo_min_amount, utxo_max_amount, + settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date, + max_tx_weight, updated_at +) VALUES ( + @id, @ban_threshold, @ban_duration, + @unilateral_exit_delay, @public_unilateral_exit_delay, + @checkpoint_exit_delay, @boarding_exit_delay, + @vtxo_tree_expiry, + @round_min_participants_count, @round_max_participants_count, + @vtxo_min_amount, @vtxo_max_amount, + @utxo_min_amount, @utxo_max_amount, + @settlement_min_expiry_gap, + @vtxo_no_csv_validation_cutoff_date, + @max_tx_weight, @updated_at +) +ON CONFLICT(id) DO UPDATE SET + ban_threshold = EXCLUDED.ban_threshold, + ban_duration = EXCLUDED.ban_duration, + unilateral_exit_delay = EXCLUDED.unilateral_exit_delay, + public_unilateral_exit_delay = EXCLUDED.public_unilateral_exit_delay, + checkpoint_exit_delay = EXCLUDED.checkpoint_exit_delay, + boarding_exit_delay = EXCLUDED.boarding_exit_delay, + vtxo_tree_expiry = EXCLUDED.vtxo_tree_expiry, + round_min_participants_count = EXCLUDED.round_min_participants_count, + round_max_participants_count = EXCLUDED.round_max_participants_count, + vtxo_min_amount = EXCLUDED.vtxo_min_amount, + vtxo_max_amount = EXCLUDED.vtxo_max_amount, + utxo_min_amount = EXCLUDED.utxo_min_amount, + utxo_max_amount = EXCLUDED.utxo_max_amount, + settlement_min_expiry_gap = EXCLUDED.settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date = EXCLUDED.vtxo_no_csv_validation_cutoff_date, + max_tx_weight = EXCLUDED.max_tx_weight, + updated_at = EXCLUDED.updated_at; + +-- name: SelectLatestSettings :one +SELECT * FROM settings ORDER BY updated_at DESC LIMIT 1; + +-- name: ClearSettings :exec +DELETE FROM settings; \ No newline at end of file diff --git a/internal/infrastructure/db/service.go b/internal/infrastructure/db/service.go index eb251ac71..93ea4009c 100644 --- a/internal/infrastructure/db/service.go +++ b/internal/infrastructure/db/service.go @@ -78,6 +78,11 @@ var ( "sqlite": sqlitedb.NewIntentFeesRepository, "postgres": pgdb.NewIntentFeesRepository, } + settingsStoreTypes = map[string]func(...interface{}) (domain.SettingsRepository, error){ + "badger": badgerdb.NewSettingsRepository, + "sqlite": sqlitedb.NewSettingsRepository, + "postgres": pgdb.NewSettingsRepository, + } ) const ( @@ -101,6 +106,7 @@ type service struct { convictionStore domain.ConvictionRepository assetStore domain.AssetRepository intentFeesStore domain.FeeRepository + settingsStore domain.SettingsRepository txDecoder ports.TxDecoder batchUpdateHandler *updateHandler[domain.Round] offchainTxUpdateHandler *updateHandler[domain.OffchainTx] @@ -141,6 +147,11 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana return nil, fmt.Errorf("invalid data store type: %s", config.DataStoreType) } + settingsStoreFactory, ok := settingsStoreTypes[config.DataStoreType] + if !ok { + return nil, fmt.Errorf("invalid data store type: %s", config.DataStoreType) + } + var eventStore domain.EventRepository var roundStore domain.RoundRepository var vtxoStore domain.VtxoRepository @@ -149,6 +160,7 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana var convictionStore domain.ConvictionRepository var assetStore domain.AssetRepository var intentFeesStore domain.FeeRepository + var settingsStore domain.SettingsRepository var err error switch config.EventStoreType { @@ -221,6 +233,10 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana if err != nil { return nil, fmt.Errorf("failed to create intent fees store: %w", err) } + settingsStore, err = settingsStoreFactory(config.DataStoreConfig...) + if err != nil { + return nil, fmt.Errorf("failed to create settings store: %w", err) + } case "postgres": if len(config.DataStoreConfig) != 3 { return nil, fmt.Errorf("invalid data store config for postgres") @@ -301,6 +317,10 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana if err != nil { return nil, fmt.Errorf("failed to create intent fees store: %w", err) } + settingsStore, err = settingsStoreFactory(db) + if err != nil { + return nil, fmt.Errorf("failed to create settings store: %w", err) + } case "sqlite": if len(config.DataStoreConfig) != 1 { return nil, fmt.Errorf("invalid data store config") @@ -369,6 +389,10 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana if err != nil { return nil, fmt.Errorf("failed to create intent fees store: %w", err) } + settingsStore, err = settingsStoreFactory(db) + if err != nil { + return nil, fmt.Errorf("failed to create settings store: %w", err) + } } svc := &service{ @@ -381,6 +405,7 @@ func NewService(config ServiceConfig, txDecoder ports.TxDecoder) (ports.RepoMana convictionStore: convictionStore, assetStore: assetStore, intentFeesStore: intentFeesStore, + settingsStore: settingsStore, batchUpdateHandler: newUpdateHandler[domain.Round](), offchainTxUpdateHandler: newUpdateHandler[domain.OffchainTx](), } @@ -428,6 +453,10 @@ func (s *service) Fees() domain.FeeRepository { return s.intentFeesStore } +func (s *service) Settings() domain.SettingsRepository { + return s.settingsStore +} + func (s *service) RegisterBatchUpdateHandler(handler func(data domain.Round)) { s.batchUpdateHandler.set(handler) } @@ -443,6 +472,7 @@ func (s *service) Close() { s.scheduledSessionStore.Close() s.offchainTxStore.Close() s.convictionStore.Close() + s.settingsStore.Close() } func (s *service) updateProjectionsAfterRoundEvents(events []domain.Event) { diff --git a/internal/infrastructure/db/service_test.go b/internal/infrastructure/db/service_test.go index ee006e436..900924caf 100644 --- a/internal/infrastructure/db/service_test.go +++ b/internal/infrastructure/db/service_test.go @@ -189,6 +189,7 @@ func TestService(t *testing.T) { testScheduledSessionRepository(t, svc) testConvictionRepository(t, svc) testFeeRepository(t, svc) + testSettingsRepository(t, svc) svc.Close() }) @@ -1884,6 +1885,97 @@ func testFeeRepository(t *testing.T, svc ports.RepoManager) { }) } +func testSettingsRepository(t *testing.T, svc ports.RepoManager) { + t.Run("test_settings_repository", func(t *testing.T) { + ctx := context.Background() + repo := svc.Settings() + + // Get returns nil when no settings exist + settings, err := repo.Get(ctx) + require.NoError(t, err) + require.Nil(t, settings) + + now := time.Now().Truncate(time.Second) + expected := domain.Settings{ + BanThreshold: 3, + BanDuration: 3600, + UnilateralExitDelay: 512, + PublicUnilateralExitDelay: 256, + CheckpointExitDelay: 128, + BoardingExitDelay: 64, + VtxoTreeExpiry: 1024, + RoundMinParticipantsCount: 2, + RoundMaxParticipantsCount: 128, + VtxoMinAmount: 1000, + VtxoMaxAmount: 100000000, + UtxoMinAmount: 5000, + UtxoMaxAmount: 500000000, + SettlementMinExpiryGap: 7200, + VtxoNoCsvValidationCutoffDate: 1700000000, + MaxTxWeight: 400000, + UpdatedAt: now, + } + + // Upsert inserts new settings + err = repo.Upsert(ctx, expected) + require.NoError(t, err) + + got, err := repo.Get(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assertSettingsEqual(t, expected, *got) + + // Upsert updates existing settings + expected.BanThreshold = 5 + expected.BanDuration = 7200 + expected.RoundMaxParticipantsCount = 256 + expected.VtxoMinAmount = 2000 + expected.MaxTxWeight = 500000 + expected.UpdatedAt = now.Add(100 * time.Second) + + err = repo.Upsert(ctx, expected) + require.NoError(t, err) + + got, err = repo.Get(ctx) + require.NoError(t, err) + require.NotNil(t, got) + assertSettingsEqual(t, expected, *got) + + // Clear removes all settings + err = repo.Clear(ctx) + require.NoError(t, err) + + settings, err = repo.Get(ctx) + require.NoError(t, err) + require.Nil(t, settings) + + // No error if trying to clear already cleared settings + err = repo.Clear(ctx) + require.NoError(t, err) + }) +} + +func assertSettingsEqual(t *testing.T, expected, actual domain.Settings) { + t.Helper() + assert.Equal(t, expected.BanThreshold, actual.BanThreshold, "BanThreshold not equal") + assert.Equal(t, expected.BanDuration, actual.BanDuration, "BanDuration not equal") + assert.Equal(t, expected.UnilateralExitDelay, actual.UnilateralExitDelay, "UnilateralExitDelay not equal") + assert.Equal(t, expected.PublicUnilateralExitDelay, actual.PublicUnilateralExitDelay, "PublicUnilateralExitDelay not equal") + assert.Equal(t, expected.CheckpointExitDelay, actual.CheckpointExitDelay, "CheckpointExitDelay not equal") + assert.Equal(t, expected.BoardingExitDelay, actual.BoardingExitDelay, "BoardingExitDelay not equal") + assert.Equal(t, expected.VtxoTreeExpiry, actual.VtxoTreeExpiry, "VtxoTreeExpiry not equal") + assert.Equal(t, expected.RoundMinParticipantsCount, actual.RoundMinParticipantsCount, "RoundMinParticipantsCount not equal") + assert.Equal(t, expected.RoundMaxParticipantsCount, actual.RoundMaxParticipantsCount, "RoundMaxParticipantsCount not equal") + assert.Equal(t, expected.VtxoMinAmount, actual.VtxoMinAmount, "VtxoMinAmount not equal") + assert.Equal(t, expected.VtxoMaxAmount, actual.VtxoMaxAmount, "VtxoMaxAmount not equal") + assert.Equal(t, expected.UtxoMinAmount, actual.UtxoMinAmount, "UtxoMinAmount not equal") + assert.Equal(t, expected.UtxoMaxAmount, actual.UtxoMaxAmount, "UtxoMaxAmount not equal") + assert.Equal(t, expected.SettlementMinExpiryGap, actual.SettlementMinExpiryGap, "SettlementMinExpiryGap not equal") + assert.Equal(t, expected.VtxoNoCsvValidationCutoffDate, actual.VtxoNoCsvValidationCutoffDate, "VtxoNoCsvValidationCutoffDate not equal") + assert.Equal(t, expected.MaxTxWeight, actual.MaxTxWeight, "MaxTxWeight not equal") + assert.True(t, expected.UpdatedAt.Equal(actual.UpdatedAt), "UpdatedAt not equal") +} + func assertScheduledSessionEqual(t *testing.T, expected, actual domain.ScheduledSession) { assert.True(t, expected.StartTime.Equal(actual.StartTime), "StartTime not equal") assert.Equal(t, expected.Period, actual.Period, "Period not equal") diff --git a/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.down.sql b/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.down.sql new file mode 100644 index 000000000..4596c6a00 --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS settings; diff --git a/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.up.sql b/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.up.sql new file mode 100644 index 000000000..7c66c24df --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260227000000_add_settings.up.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ban_threshold BIGINT NOT NULL DEFAULT 0, + ban_duration BIGINT NOT NULL DEFAULT 0, + unilateral_exit_delay BIGINT NOT NULL DEFAULT 0, + public_unilateral_exit_delay BIGINT NOT NULL DEFAULT 0, + checkpoint_exit_delay BIGINT NOT NULL DEFAULT 0, + boarding_exit_delay BIGINT NOT NULL DEFAULT 0, + vtxo_tree_expiry BIGINT NOT NULL DEFAULT 0, + round_min_participants_count BIGINT NOT NULL DEFAULT 0, + round_max_participants_count BIGINT NOT NULL DEFAULT 0, + vtxo_min_amount BIGINT NOT NULL DEFAULT 0, + vtxo_max_amount BIGINT NOT NULL DEFAULT 0, + utxo_min_amount BIGINT NOT NULL DEFAULT 0, + utxo_max_amount BIGINT NOT NULL DEFAULT 0, + settlement_min_expiry_gap BIGINT NOT NULL DEFAULT 0, + vtxo_no_csv_validation_cutoff_date BIGINT NOT NULL DEFAULT 0, + max_tx_weight BIGINT NOT NULL DEFAULT 0, + updated_at BIGINT NOT NULL +); diff --git a/internal/infrastructure/db/sqlite/settings_repo.go b/internal/infrastructure/db/sqlite/settings_repo.go new file mode 100644 index 000000000..9a9f85b02 --- /dev/null +++ b/internal/infrastructure/db/sqlite/settings_repo.go @@ -0,0 +1,98 @@ +package sqlitedb + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/arkade-os/arkd/internal/core/domain" + "github.com/arkade-os/arkd/internal/infrastructure/db/sqlite/sqlc/queries" +) + +type settingsRepository struct { + db *sql.DB + querier *queries.Queries +} + +func NewSettingsRepository(config ...interface{}) (domain.SettingsRepository, error) { + if len(config) != 1 { + return nil, fmt.Errorf("invalid config: expected 1 argument, got %d", len(config)) + } + db, ok := config[0].(*sql.DB) + if !ok { + return nil, fmt.Errorf( + "cannot open settings repository: expected *sql.DB but got %T", config[0], + ) + } + + return &settingsRepository{ + db: db, + querier: queries.New(db), + }, nil +} + +func (r *settingsRepository) Get(ctx context.Context) (*domain.Settings, error) { + settings, err := r.querier.SelectLatestSettings(ctx) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get settings: %w", err) + } + + return &domain.Settings{ + BanThreshold: settings.BanThreshold, + BanDuration: settings.BanDuration, + UnilateralExitDelay: settings.UnilateralExitDelay, + PublicUnilateralExitDelay: settings.PublicUnilateralExitDelay, + CheckpointExitDelay: settings.CheckpointExitDelay, + BoardingExitDelay: settings.BoardingExitDelay, + VtxoTreeExpiry: settings.VtxoTreeExpiry, + RoundMinParticipantsCount: settings.RoundMinParticipantsCount, + RoundMaxParticipantsCount: settings.RoundMaxParticipantsCount, + VtxoMinAmount: settings.VtxoMinAmount, + VtxoMaxAmount: settings.VtxoMaxAmount, + UtxoMinAmount: settings.UtxoMinAmount, + UtxoMaxAmount: settings.UtxoMaxAmount, + SettlementMinExpiryGap: settings.SettlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: settings.VtxoNoCsvValidationCutoffDate, + MaxTxWeight: settings.MaxTxWeight, + UpdatedAt: time.Unix(settings.UpdatedAt, 0), + }, nil +} + +// Upsert inserts or updates the singleton settings row. The ID field defaults +// to 0 (Go zero value) which is used as the fixed singleton key via ON CONFLICT(id). +func (r *settingsRepository) Upsert( + ctx context.Context, settings domain.Settings, +) error { + return r.querier.UpsertSettings(ctx, queries.UpsertSettingsParams{ + BanThreshold: settings.BanThreshold, + BanDuration: settings.BanDuration, + UnilateralExitDelay: settings.UnilateralExitDelay, + PublicUnilateralExitDelay: settings.PublicUnilateralExitDelay, + CheckpointExitDelay: settings.CheckpointExitDelay, + BoardingExitDelay: settings.BoardingExitDelay, + VtxoTreeExpiry: settings.VtxoTreeExpiry, + RoundMinParticipantsCount: settings.RoundMinParticipantsCount, + RoundMaxParticipantsCount: settings.RoundMaxParticipantsCount, + VtxoMinAmount: settings.VtxoMinAmount, + VtxoMaxAmount: settings.VtxoMaxAmount, + UtxoMinAmount: settings.UtxoMinAmount, + UtxoMaxAmount: settings.UtxoMaxAmount, + SettlementMinExpiryGap: settings.SettlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: settings.VtxoNoCsvValidationCutoffDate, + MaxTxWeight: settings.MaxTxWeight, + UpdatedAt: settings.UpdatedAt.Unix(), + }) +} + +func (r *settingsRepository) Clear(ctx context.Context) error { + return r.querier.ClearSettings(ctx) +} + +func (r *settingsRepository) Close() { + _ = r.db.Close() +} diff --git a/internal/infrastructure/db/sqlite/sqlc/queries/models.go b/internal/infrastructure/db/sqlite/sqlc/queries/models.go index f5ded82b3..16e83d91b 100644 --- a/internal/infrastructure/db/sqlite/sqlc/queries/models.go +++ b/internal/infrastructure/db/sqlite/sqlc/queries/models.go @@ -193,6 +193,27 @@ type ScheduledSession struct { UpdatedAt int64 } +type Setting struct { + ID int64 + BanThreshold int64 + BanDuration int64 + UnilateralExitDelay int64 + PublicUnilateralExitDelay int64 + CheckpointExitDelay int64 + BoardingExitDelay int64 + VtxoTreeExpiry int64 + RoundMinParticipantsCount int64 + RoundMaxParticipantsCount int64 + VtxoMinAmount int64 + VtxoMaxAmount int64 + UtxoMinAmount int64 + UtxoMaxAmount int64 + SettlementMinExpiryGap int64 + VtxoNoCsvValidationCutoffDate int64 + MaxTxWeight int64 + UpdatedAt int64 +} + type Tx struct { Txid string Tx string diff --git a/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go b/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go index 0bb137d25..edd45b8b4 100644 --- a/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go +++ b/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go @@ -84,6 +84,15 @@ func (q *Queries) ClearScheduledSession(ctx context.Context) error { return err } +const clearSettings = `-- name: ClearSettings :exec +DELETE FROM settings +` + +func (q *Queries) ClearSettings(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, clearSettings) + return err +} + const insertAsset = `-- name: InsertAsset :exec INSERT INTO asset (id, is_immutable, metadata_hash, metadata, control_asset_id) VALUES (?1, ?2, ?3, ?4, ?5) @@ -594,6 +603,36 @@ func (q *Queries) SelectLatestScheduledSession(ctx context.Context) (ScheduledSe return i, err } +const selectLatestSettings = `-- name: SelectLatestSettings :one +SELECT id, ban_threshold, ban_duration, unilateral_exit_delay, public_unilateral_exit_delay, checkpoint_exit_delay, boarding_exit_delay, vtxo_tree_expiry, round_min_participants_count, round_max_participants_count, vtxo_min_amount, vtxo_max_amount, utxo_min_amount, utxo_max_amount, settlement_min_expiry_gap, vtxo_no_csv_validation_cutoff_date, max_tx_weight, updated_at FROM settings ORDER BY updated_at DESC LIMIT 1 +` + +func (q *Queries) SelectLatestSettings(ctx context.Context) (Setting, error) { + row := q.db.QueryRowContext(ctx, selectLatestSettings) + var i Setting + err := row.Scan( + &i.ID, + &i.BanThreshold, + &i.BanDuration, + &i.UnilateralExitDelay, + &i.PublicUnilateralExitDelay, + &i.CheckpointExitDelay, + &i.BoardingExitDelay, + &i.VtxoTreeExpiry, + &i.RoundMinParticipantsCount, + &i.RoundMaxParticipantsCount, + &i.VtxoMinAmount, + &i.VtxoMaxAmount, + &i.UtxoMinAmount, + &i.UtxoMaxAmount, + &i.SettlementMinExpiryGap, + &i.VtxoNoCsvValidationCutoffDate, + &i.MaxTxWeight, + &i.UpdatedAt, + ) + return i, err +} + const selectNotUnrolledVtxos = `-- name: SelectNotUnrolledVtxos :many SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.swept, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.commitments, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false ` @@ -2262,6 +2301,95 @@ func (q *Queries) UpsertScheduledSession(ctx context.Context, arg UpsertSchedule return err } +const upsertSettings = `-- name: UpsertSettings :exec +INSERT INTO settings ( + id, ban_threshold, ban_duration, + unilateral_exit_delay, public_unilateral_exit_delay, + checkpoint_exit_delay, boarding_exit_delay, + vtxo_tree_expiry, + round_min_participants_count, round_max_participants_count, + vtxo_min_amount, vtxo_max_amount, + utxo_min_amount, utxo_max_amount, + settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date, + max_tx_weight, updated_at +) VALUES ( + ?1, ?2, ?3, + ?4, ?5, + ?6, ?7, + ?8, + ?9, ?10, + ?11, ?12, + ?13, ?14, + ?15, + ?16, + ?17, ?18 +) +ON CONFLICT(id) DO UPDATE SET + ban_threshold = EXCLUDED.ban_threshold, + ban_duration = EXCLUDED.ban_duration, + unilateral_exit_delay = EXCLUDED.unilateral_exit_delay, + public_unilateral_exit_delay = EXCLUDED.public_unilateral_exit_delay, + checkpoint_exit_delay = EXCLUDED.checkpoint_exit_delay, + boarding_exit_delay = EXCLUDED.boarding_exit_delay, + vtxo_tree_expiry = EXCLUDED.vtxo_tree_expiry, + round_min_participants_count = EXCLUDED.round_min_participants_count, + round_max_participants_count = EXCLUDED.round_max_participants_count, + vtxo_min_amount = EXCLUDED.vtxo_min_amount, + vtxo_max_amount = EXCLUDED.vtxo_max_amount, + utxo_min_amount = EXCLUDED.utxo_min_amount, + utxo_max_amount = EXCLUDED.utxo_max_amount, + settlement_min_expiry_gap = EXCLUDED.settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date = EXCLUDED.vtxo_no_csv_validation_cutoff_date, + max_tx_weight = EXCLUDED.max_tx_weight, + updated_at = EXCLUDED.updated_at +` + +type UpsertSettingsParams struct { + ID int64 + BanThreshold int64 + BanDuration int64 + UnilateralExitDelay int64 + PublicUnilateralExitDelay int64 + CheckpointExitDelay int64 + BoardingExitDelay int64 + VtxoTreeExpiry int64 + RoundMinParticipantsCount int64 + RoundMaxParticipantsCount int64 + VtxoMinAmount int64 + VtxoMaxAmount int64 + UtxoMinAmount int64 + UtxoMaxAmount int64 + SettlementMinExpiryGap int64 + VtxoNoCsvValidationCutoffDate int64 + MaxTxWeight int64 + UpdatedAt int64 +} + +func (q *Queries) UpsertSettings(ctx context.Context, arg UpsertSettingsParams) error { + _, err := q.db.ExecContext(ctx, upsertSettings, + arg.ID, + arg.BanThreshold, + arg.BanDuration, + arg.UnilateralExitDelay, + arg.PublicUnilateralExitDelay, + arg.CheckpointExitDelay, + arg.BoardingExitDelay, + arg.VtxoTreeExpiry, + arg.RoundMinParticipantsCount, + arg.RoundMaxParticipantsCount, + arg.VtxoMinAmount, + arg.VtxoMaxAmount, + arg.UtxoMinAmount, + arg.UtxoMaxAmount, + arg.SettlementMinExpiryGap, + arg.VtxoNoCsvValidationCutoffDate, + arg.MaxTxWeight, + arg.UpdatedAt, + ) + return err +} + const upsertTx = `-- name: UpsertTx :exec INSERT INTO tx (tx, round_id, type, position, txid, children) VALUES (?1, ?2, ?3, ?4, ?5, ?6) diff --git a/internal/infrastructure/db/sqlite/sqlc/query.sql b/internal/infrastructure/db/sqlite/sqlc/query.sql index f4e2e9fe1..1557e5dff 100644 --- a/internal/infrastructure/db/sqlite/sqlc/query.sql +++ b/internal/infrastructure/db/sqlite/sqlc/query.sql @@ -454,4 +454,53 @@ WHERE v.asset_id = ? AND v.spent = false AND v.asset_amount > 0; SELECT control_asset_id FROM asset WHERE id = ?; -- name: SelectAssetExists :one -SELECT 1 FROM asset WHERE id = ? LIMIT 1; \ No newline at end of file +SELECT 1 FROM asset WHERE id = ? LIMIT 1; + +-- name: UpsertSettings :exec +INSERT INTO settings ( + id, ban_threshold, ban_duration, + unilateral_exit_delay, public_unilateral_exit_delay, + checkpoint_exit_delay, boarding_exit_delay, + vtxo_tree_expiry, + round_min_participants_count, round_max_participants_count, + vtxo_min_amount, vtxo_max_amount, + utxo_min_amount, utxo_max_amount, + settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date, + max_tx_weight, updated_at +) VALUES ( + @id, @ban_threshold, @ban_duration, + @unilateral_exit_delay, @public_unilateral_exit_delay, + @checkpoint_exit_delay, @boarding_exit_delay, + @vtxo_tree_expiry, + @round_min_participants_count, @round_max_participants_count, + @vtxo_min_amount, @vtxo_max_amount, + @utxo_min_amount, @utxo_max_amount, + @settlement_min_expiry_gap, + @vtxo_no_csv_validation_cutoff_date, + @max_tx_weight, @updated_at +) +ON CONFLICT(id) DO UPDATE SET + ban_threshold = EXCLUDED.ban_threshold, + ban_duration = EXCLUDED.ban_duration, + unilateral_exit_delay = EXCLUDED.unilateral_exit_delay, + public_unilateral_exit_delay = EXCLUDED.public_unilateral_exit_delay, + checkpoint_exit_delay = EXCLUDED.checkpoint_exit_delay, + boarding_exit_delay = EXCLUDED.boarding_exit_delay, + vtxo_tree_expiry = EXCLUDED.vtxo_tree_expiry, + round_min_participants_count = EXCLUDED.round_min_participants_count, + round_max_participants_count = EXCLUDED.round_max_participants_count, + vtxo_min_amount = EXCLUDED.vtxo_min_amount, + vtxo_max_amount = EXCLUDED.vtxo_max_amount, + utxo_min_amount = EXCLUDED.utxo_min_amount, + utxo_max_amount = EXCLUDED.utxo_max_amount, + settlement_min_expiry_gap = EXCLUDED.settlement_min_expiry_gap, + vtxo_no_csv_validation_cutoff_date = EXCLUDED.vtxo_no_csv_validation_cutoff_date, + max_tx_weight = EXCLUDED.max_tx_weight, + updated_at = EXCLUDED.updated_at; + +-- name: SelectLatestSettings :one +SELECT * FROM settings ORDER BY updated_at DESC LIMIT 1; + +-- name: ClearSettings :exec +DELETE FROM settings; \ No newline at end of file diff --git a/internal/interface/grpc/handlers/adminservice.go b/internal/interface/grpc/handlers/adminservice.go index c65f29b13..0143859e2 100644 --- a/internal/interface/grpc/handlers/adminservice.go +++ b/internal/interface/grpc/handlers/adminservice.go @@ -617,6 +617,88 @@ func (a *adminHandler) ClearIntentFees( return &arkv1.ClearIntentFeesResponse{}, nil } +func (a *adminHandler) GetSettings( + ctx context.Context, _ *arkv1.GetSettingsRequest, +) (*arkv1.GetSettingsResponse, error) { + settings, err := a.adminService.GetSettings(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "%s", err.Error()) + } + + var protoSettings *arkv1.Settings + if settings != nil { + protoSettings = &arkv1.Settings{ + BanThreshold: settings.BanThreshold, + BanDuration: settings.BanDuration, + UnilateralExitDelay: settings.UnilateralExitDelay, + PublicUnilateralExitDelay: settings.PublicUnilateralExitDelay, + CheckpointExitDelay: settings.CheckpointExitDelay, + BoardingExitDelay: settings.BoardingExitDelay, + VtxoTreeExpiry: settings.VtxoTreeExpiry, + RoundMinParticipantsCount: settings.RoundMinParticipantsCount, + RoundMaxParticipantsCount: settings.RoundMaxParticipantsCount, + VtxoMinAmount: settings.VtxoMinAmount, + VtxoMaxAmount: settings.VtxoMaxAmount, + UtxoMinAmount: settings.UtxoMinAmount, + UtxoMaxAmount: settings.UtxoMaxAmount, + SettlementMinExpiryGap: settings.SettlementMinExpiryGap, + VtxoNoCsvValidationCutoffDate: settings.VtxoNoCsvValidationCutoffDate, + MaxTxWeight: settings.MaxTxWeight, + UpdatedAt: settings.UpdatedAt.Unix(), + } + } + + return &arkv1.GetSettingsResponse{Settings: protoSettings}, nil +} + +func (a *adminHandler) UpdateSettings( + ctx context.Context, req *arkv1.UpdateSettingsRequest, +) (*arkv1.UpdateSettingsResponse, error) { + s := req.GetSettings() + if s == nil { + return nil, status.Error(codes.InvalidArgument, "missing settings") + } + + settings := domain.Settings{ + BanThreshold: s.GetBanThreshold(), + BanDuration: s.GetBanDuration(), + UnilateralExitDelay: s.GetUnilateralExitDelay(), + PublicUnilateralExitDelay: s.GetPublicUnilateralExitDelay(), + CheckpointExitDelay: s.GetCheckpointExitDelay(), + BoardingExitDelay: s.GetBoardingExitDelay(), + VtxoTreeExpiry: s.GetVtxoTreeExpiry(), + RoundMinParticipantsCount: s.GetRoundMinParticipantsCount(), + RoundMaxParticipantsCount: s.GetRoundMaxParticipantsCount(), + VtxoMinAmount: s.GetVtxoMinAmount(), + VtxoMaxAmount: s.GetVtxoMaxAmount(), + UtxoMinAmount: s.GetUtxoMinAmount(), + UtxoMaxAmount: s.GetUtxoMaxAmount(), + SettlementMinExpiryGap: s.GetSettlementMinExpiryGap(), + VtxoNoCsvValidationCutoffDate: s.GetVtxoNoCsvValidationCutoffDate(), + MaxTxWeight: s.GetMaxTxWeight(), + } + + if err := a.adminService.UpdateSettings(ctx, settings, req.GetUpdateFields()); err != nil { + var validationErr *domain.ErrInvalidSettings + if errors.As(err, &validationErr) { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return nil, status.Errorf(codes.Internal, "%s", err.Error()) + } + + return &arkv1.UpdateSettingsResponse{}, nil +} + +func (a *adminHandler) ClearSettings( + ctx context.Context, _ *arkv1.ClearSettingsRequest, +) (*arkv1.ClearSettingsResponse, error) { + if err := a.adminService.ClearSettings(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "%s", err.Error()) + } + + return &arkv1.ClearSettingsResponse{}, nil +} + func convertConvictionToProto(conviction domain.Conviction) (*arkv1.Conviction, error) { var expiresAt int64 if conviction.GetExpiresAt() != nil { diff --git a/internal/interface/grpc/permissions/permissions.go b/internal/interface/grpc/permissions/permissions.go index 95625971d..623f12f71 100644 --- a/internal/interface/grpc/permissions/permissions.go +++ b/internal/interface/grpc/permissions/permissions.go @@ -379,6 +379,18 @@ func AllPermissionsByMethod() map[string][]bakery.Op { Entity: EntityAuthManager, Action: "write", }}, + fmt.Sprintf("/%s/GetSettings", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "read", + }}, + fmt.Sprintf("/%s/UpdateSettings", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "write", + }}, + fmt.Sprintf("/%s/ClearSettings", arkv1.AdminService_ServiceDesc.ServiceName): {{ + Entity: EntityManager, + Action: "write", + }}, fmt.Sprintf("/%s/ListTokens", arkv1.AdminService_ServiceDesc.ServiceName): {{ Entity: EntityManager, Action: "read", diff --git a/internal/test/e2e/single_batch_smoke_test.go b/internal/test/e2e/single_batch_smoke_test.go index e5a6a4cf8..c9b564a12 100644 --- a/internal/test/e2e/single_batch_smoke_test.go +++ b/internal/test/e2e/single_batch_smoke_test.go @@ -24,8 +24,8 @@ import ( type singleBatchConfig struct { NumClients int // Number of clients to participate in the batch AmountPerVtxo uint64 // Amount in satoshis per VTXO - MinParticipantsPerRound int // Minimum number of participants per round (ARKD_ROUND_MIN_PARTICIPANTS_COUNT) - MaxParticipantsPerRound int // Maximum number of participants per round (ARKD_ROUND_MAX_PARTICIPANTS_COUNT) + MinParticipantsPerRound int // Minimum number of participants per round + MaxParticipantsPerRound int // Maximum number of participants per round } var ( @@ -42,13 +42,7 @@ var ( // TestBatchSettleMultipleClients tests multiple clients registering VTXOs in a single batch // // This test verifies that multiple clients can register VTXOs in a single batch settlement round. -// It is affected by the following environment variables: -// - ARKD_ROUND_MIN_PARTICIPANTS_COUNT: Minimum number of participants per round (default: 1) -// - ARKD_ROUND_MAX_PARTICIPANTS_COUNT: Maximum number of participants per round (default: 128) -// -// To run this test with specific round participant limits, set these environment variables before running. -// For example, to test with exactly 5 participants per round: -// ARKD_ROUND_MIN_PARTICIPANTS_COUNT=5 ARKD_ROUND_MAX_PARTICIPANTS_COUNT=5 make run-simulation +// Round participant limits are configured via the admin settings API. // // To specify the number of clients via command line: // go test -v -run TestBatchSettleMultipleClients -args -num-clients=8