diff --git a/apps/consent/package.json b/apps/consent/package.json index 192a6f7731..96a2b3048a 100644 --- a/apps/consent/package.json +++ b/apps/consent/package.json @@ -28,7 +28,7 @@ "@opentelemetry/semantic-conventions": "1.27.0", "@ory/hydra-client": "^2.2.0", "@t3-oss/env-nextjs": "^0.7.1", - "axios": "^1.11.0", + "axios": "^1.15.0", "dotenv": "^16.3.1", "edge-csrf": "^1.0.6", "graphql": "^16.8.1", diff --git a/bats/core/api-keys/api-keys-hold-invoice-reversal.bats b/bats/core/api-keys/api-keys-hold-invoice-reversal.bats new file mode 100644 index 0000000000..2bbd95b35b --- /dev/null +++ b/bats/core/api-keys/api-keys-hold-invoice-reversal.bats @@ -0,0 +1,98 @@ +#!/usr/bin/env bats + +# Tests for API key spending reversal when hold invoices are canceled/timeout + +load "../../helpers/user.bash" +load "../../helpers/onchain.bash" +load "../../helpers/ln.bash" + +ALICE='alice' + +setup_file() { + clear_cache + + # Ensure LND has sufficient balance for lightning tests + lnd1_balance=$(lnd_cli channelbalance | jq -r '.balance // 0') + if [[ $lnd1_balance -lt "1000000" ]]; then + create_user 'lnd_funding' + fund_user_lightning 'lnd_funding' 'lnd_funding.btc_wallet_id' '5000000' + fi + + create_user "$ALICE" + fund_user_onchain "$ALICE" 'btc_wallet' +} + +@test "hold-invoice-reversal: create api key with daily limit" { + key_name="$(random_uuid)" + cache_value 'hold_invoice_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + + exec_graphql 'alice' 'api-key-create' "$variables" + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + + cache_value "api-key-hold-invoice-secret" "$secret" + + name=$(echo "$key" | jq -r '.name') + [[ "${name}" = "${key_name}" ]] || exit 1 + + key_id=$(echo "$key" | jq -r '.id') + cache_value "hold-invoice-api-key-id" "$key_id" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":10000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + daily_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.dailyLimitSats')" + [[ "${daily_limit}" = "10000" ]] || exit 1 + + spent_24h="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.dailySpentSats')" + [[ "${spent_24h}" = "0" ]] || exit 1 +} + +@test "hold-invoice-reversal: pay hold invoice and check spending recorded" { + secret=$(xxd -l 32 -c 256 -p /dev/urandom) + payment_hash=$(echo -n $secret | xxd -r -p | sha256sum | cut -d ' ' -f1) + + cache_value "hold-invoice-preimage" "$secret" + cache_value "hold-invoice-payment-hash" "$payment_hash" + + invoice_response="$(lnd_outside_cli addholdinvoice "$payment_hash" --amt 5000 --memo 'Test hold invoice')" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + + cache_value "hold-invoice-payment-request" "$payment_request" + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg payment_request "$payment_request" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request}}' + ) + + exec_graphql 'api-key-hold-invoice-secret' 'ln-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnInvoicePaymentSend.status')" + [[ "${send_status}" = "PENDING" || "${send_status}" = "SUCCESS" ]] || exit 1 + + invoice_info="$(lnd_outside_cli lookupinvoice "$payment_hash")" + state="$(echo "$invoice_info" | jq -r '.state')" + [[ "${state}" = "ACCEPTED" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'hold_invoice_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "5000" ]] || exit 1 +} + +@test "hold-invoice-reversal: verify spending was reversed after cancellation" { + payment_hash="$(read_value 'hold-invoice-payment-hash')" + + lnd_outside_cli cancelinvoice "$payment_hash" + + check_spending_reversed() { + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'hold_invoice_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" = "0" ]] || exit 1 + } + + # Poll until the trigger server processes the cancellation and reverses spending + retry 30 1 check_spending_reversed +} diff --git a/bats/core/api-keys/api-keys-limits.bats b/bats/core/api-keys/api-keys-limits.bats new file mode 100644 index 0000000000..c5a40eff1c --- /dev/null +++ b/bats/core/api-keys/api-keys-limits.bats @@ -0,0 +1,663 @@ +#!/usr/bin/env bats + +load "../../helpers/user.bash" +load "../../helpers/onchain.bash" +load "../../helpers/ln.bash" + +ALICE='alice' +BOB='bob' + +setup_file() { + clear_cache + + # Ensure LND has sufficient balance for lightning tests + lnd1_balance=$(lnd_cli channelbalance | jq -r '.balance // 0') + if [[ $lnd1_balance -lt "1000000" ]]; then + create_user 'lnd_funding' + fund_user_lightning 'lnd_funding' 'lnd_funding.btc_wallet_id' '5000000' + fi + + create_user "$ALICE" + fund_user_onchain "$ALICE" 'btc_wallet' + fund_user_onchain "$ALICE" 'usd_wallet' + + create_user "$BOB" + user_update_username "$BOB" + # xyz_zap_receiver is a user whose lnurl address is hardcoded in the lnurlPaymentSend tests below. + ensure_username_is_present "xyz_zap_receiver" +} + +@test "api-keys-limits: create key and set daily limit" { + key_name="$(random_uuid)" + cache_value 'limit_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + + exec_graphql 'alice' 'api-key-create' "$variables" + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + + cache_value "api-key-limit-secret" "$secret" + + name=$(echo "$key" | jq -r '.name') + [[ "${name}" = "${key_name}" ]] || exit 1 + + key_id=$(echo "$key" | jq -r '.id') + cache_value "limit-api-key-id" "$key_id" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":10000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + daily_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.dailyLimitSats')" + [[ "${daily_limit}" = "10000" ]] || exit 1 + + spent_24h="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.dailySpentSats')" + [[ "${spent_24h}" = "0" ]] || exit 1 +} + +@test "api-keys-limits: can send payment within limit" { + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=5000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "$amount" ]] || exit 1 +} + +@test "api-keys-limits: cannot exceed daily limit" { + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=6000 # Would exceed 10000 daily limit + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + errors="$(graphql_output '.data.intraLedgerPaymentSend.errors | length')" + [[ "${errors}" -ge "1" ]] || exit 1 + + # Verify error code and message both indicate a daily spending limit restriction + error_code="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: set weekly limit" { + key_id=$(read_value "limit-api-key-id") + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"WEEKLY\",\"limitSats\":50000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + weekly_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.weeklyLimitSats')" + [[ "${weekly_limit}" = "50000" ]] || exit 1 +} + +@test "api-keys-limits: remove daily limit" { + key_id=$(read_value "limit-api-key-id") + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\"}}" + exec_graphql 'alice' 'api-key-remove-limit' "$variables" + + daily_limit="$(graphql_output '.data.apiKeyRemoveLimit.apiKey.limits.dailyLimitSats')" + [[ "${daily_limit}" = "null" ]] || exit 1 +} + +@test "api-keys-limits: can send after removing daily limit (but weekly still applies)" { + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=3000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_7d="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.weeklySpentSats')" + [[ "${spent_7d}" -ge "8000" ]] || exit 1 +} + +@test "api-keys-limits: set monthly and annual limits" { + key_id=$(read_value "limit-api-key-id") + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"MONTHLY\",\"limitSats\":100000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + monthly_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.monthlyLimitSats')" + [[ "${monthly_limit}" = "100000" ]] || exit 1 + + spent_30d="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.monthlySpentSats')" + [[ "${spent_30d}" -ge "8000" ]] || exit 1 + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"ANNUAL\",\"limitSats\":500000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + annual_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.annualLimitSats')" + [[ "${annual_limit}" = "500000" ]] || exit 1 + + spent_365d="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.annualSpentSats')" + [[ "${spent_365d}" -ge "8000" ]] || exit 1 +} + +@test "api-keys-limits: multiple limits active - respects most restrictive" { + # At this point we have: + # - No daily limit (removed) + # - Weekly: 50000 sats (spent: ~8000) + # - Monthly: 100000 sats (spent: ~8000) + # - Annual: 500000 sats (spent: ~8000) + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=45000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a weekly spending limit restriction + error_code="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"weekly"* ]] || exit 1 +} + +@test "api-keys-limits: can send within all active limits" { + # Send 30000 sats - within weekly (50000 - 8000 = 42000 remaining) + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=30000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + key_data="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'")')" + + spent_24h=$(echo "$key_data" | jq -r '.limits.dailySpentSats') + spent_7d=$(echo "$key_data" | jq -r '.limits.weeklySpentSats') + spent_30d=$(echo "$key_data" | jq -r '.limits.monthlySpentSats') + spent_365d=$(echo "$key_data" | jq -r '.limits.annualSpentSats') + + [[ "${spent_24h}" -ge "30000" ]] || exit 1 + [[ "${spent_7d}" -ge "38000" ]] || exit 1 + [[ "${spent_30d}" -ge "38000" ]] || exit 1 + [[ "${spent_365d}" -ge "38000" ]] || exit 1 +} + +@test "api-keys-limits: spending tracked consistently across time windows" { + exec_graphql 'alice' 'api-keys' + key_data="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'")')" + + daily_limit=$(echo "$key_data" | jq -r '.limits.dailyLimitSats') + weekly_limit=$(echo "$key_data" | jq -r '.limits.weeklyLimitSats') + monthly_limit=$(echo "$key_data" | jq -r '.limits.monthlyLimitSats') + annual_limit=$(echo "$key_data" | jq -r '.limits.annualLimitSats') + + [[ "${daily_limit}" = "null" ]] || exit 1 + [[ "${weekly_limit}" = "50000" ]] || exit 1 + [[ "${monthly_limit}" = "100000" ]] || exit 1 + [[ "${annual_limit}" = "500000" ]] || exit 1 + + # All payments occurred within the last 24h so all windows should show the same total + spent_24h=$(echo "$key_data" | jq -r '.limits.dailySpentSats') + spent_7d=$(echo "$key_data" | jq -r '.limits.weeklySpentSats') + spent_30d=$(echo "$key_data" | jq -r '.limits.monthlySpentSats') + spent_365d=$(echo "$key_data" | jq -r '.limits.annualSpentSats') + + [[ "${spent_24h}" = "${spent_7d}" ]] || exit 1 + [[ "${spent_7d}" = "${spent_30d}" ]] || exit 1 + [[ "${spent_30d}" = "${spent_365d}" ]] || exit 1 +} + +@test "api-keys-limits: update existing limit to lower value" { + key_id=$(read_value "limit-api-key-id") + + # Update weekly limit to 40000 (already spent ~38000) + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"WEEKLY\",\"limitSats\":40000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + weekly_limit="$(graphql_output '.data.apiKeySetLimit.apiKey.limits.weeklyLimitSats')" + [[ "${weekly_limit}" = "40000" ]] || exit 1 + + # 3000 sats would exceed the newly lowered limit + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=3000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a weekly spending limit restriction + error_code="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.intraLedgerPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"weekly"* ]] || exit 1 +} + +@test "api-keys-limits: remove all limits" { + key_id=$(read_value "limit-api-key-id") + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"WEEKLY\"}}" + exec_graphql 'alice' 'api-key-remove-limit' "$variables" + weekly_limit="$(graphql_output '.data.apiKeyRemoveLimit.apiKey.limits.weeklyLimitSats')" + [[ "${weekly_limit}" = "null" ]] || exit 1 + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"MONTHLY\"}}" + exec_graphql 'alice' 'api-key-remove-limit' "$variables" + monthly_limit="$(graphql_output '.data.apiKeyRemoveLimit.apiKey.limits.monthlyLimitSats')" + [[ "${monthly_limit}" = "null" ]] || exit 1 + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"ANNUAL\"}}" + exec_graphql 'alice' 'api-key-remove-limit' "$variables" + annual_limit="$(graphql_output '.data.apiKeyRemoveLimit.apiKey.limits.annualLimitSats')" + [[ "${annual_limit}" = "null" ]] || exit 1 +} + +@test "api-keys-limits: can send large amount with no limits" { + local from_wallet_name="$ALICE.btc_wallet_id" + local to_wallet_name="$BOB.btc_wallet_id" + local amount=100000 + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $from_wallet_name)" \ + --arg recipient_wallet_id "$(read_value $to_wallet_name)" \ + --arg amount "$amount" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-limit-secret' 'intraledger-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + # Spending should still be tracked even without limits + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "130000" ]] || exit 1 +} + +# ============================================================================ +# Tests for different payment flows (lightning, on-chain, lnurl, no-amount invoices) +# ============================================================================ + +@test "api-keys-limits: lightning payment respects limits" { + key_name="$(random_uuid)" + cache_value 'ln_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-ln-secret" "$secret" + cache_value "ln-api-key-id" "$key_id" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":5000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + invoice_response="$(lnd_outside_cli addinvoice --amt 3000)" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + payment_hash=$(echo "$invoice_response" | jq -r '.r_hash') + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg payment_request "$payment_request" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request}}' + ) + + exec_graphql 'api-key-ln-secret' 'ln-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnInvoicePaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'ln_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "3000" ]] || exit 1 +} + +@test "api-keys-limits: lightning payment exceeding limit fails" { + # Try to send 3000 more sats (would exceed 5000 limit) + invoice_response="$(lnd_outside_cli addinvoice --amt 3000)" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg payment_request "$payment_request" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request}}' + ) + + exec_graphql 'api-key-ln-secret' 'ln-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnInvoicePaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a daily spending limit restriction + error_code="$(graphql_output '.data.lnInvoicePaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.lnInvoicePaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: onchain payment respects limits" { + key_name="$(random_uuid)" + cache_value 'onchain_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-onchain-secret" "$secret" + cache_value "onchain-api-key-id" "$key_id" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":10000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + onchain_address=$(bitcoin_cli getnewaddress) + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg address "$onchain_address" \ + --argjson amount "5000" \ + '{input: {walletId: $wallet_id, address: $address, amount: $amount}}' + ) + + exec_graphql 'api-key-onchain-secret' 'on-chain-payment-send' "$variables" + send_status="$(graphql_output '.data.onChainPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'onchain_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "5000" ]] || exit 1 +} + +@test "api-keys-limits: onchain payment exceeding limit fails" { + # Try to send 6000 more sats (would exceed 10000 limit) + onchain_address=$(bitcoin_cli getnewaddress) + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg address "$onchain_address" \ + --argjson amount "6000" \ + '{input: {walletId: $wallet_id, address: $address, amount: $amount}}' + ) + + exec_graphql 'api-key-onchain-secret' 'on-chain-payment-send' "$variables" + send_status="$(graphql_output '.data.onChainPaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a daily spending limit restriction + error_code="$(graphql_output '.data.onChainPaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.onChainPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: mixed payment flows tracked separately per key" { + exec_graphql 'alice' 'api-keys' + + # Check intraledger key spending (original key from earlier tests) + intraledger_spent="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.dailySpentSats')" + [[ "${intraledger_spent}" -ge "130000" ]] || exit 1 + + ln_spent="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'ln_key_name')'") | .limits.dailySpentSats')" + [[ "${ln_spent}" -ge "3000" ]] || exit 1 + [[ "${ln_spent}" -lt "10000" ]] || exit 1 + + onchain_spent="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'onchain_key_name')'") | .limits.dailySpentSats')" + [[ "${onchain_spent}" -ge "5000" ]] || exit 1 + [[ "${onchain_spent}" -lt "10000" ]] || exit 1 + + [[ "${intraledger_spent}" != "${ln_spent}" ]] || exit 1 + [[ "${intraledger_spent}" != "${onchain_spent}" ]] || exit 1 +} + +@test "api-keys-limits: USD wallet payments also respect limits" { + key_name="$(random_uuid)" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-usd-secret" "$secret" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":50000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Send USD intraledger payment (amount in cents) + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.usd_wallet_id)" \ + --arg recipient_wallet_id "$(read_value $BOB.usd_wallet_id)" \ + --argjson amount "25" \ + '{input: {walletId: $wallet_id, recipientWalletId: $recipient_wallet_id, amount: $amount}}' + ) + + exec_graphql 'api-key-usd-secret' 'intraledger-usd-payment-send' "$variables" + send_status="$(graphql_output '.data.intraLedgerUsdPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$key_name'") | .limits.dailySpentSats')" + # USD amount converted to sats should be tracked + [[ "${spent_24h}" -gt "0" ]] || exit 1 +} + +@test "api-keys-limits: lnNoAmountInvoicePaymentSend respects limits" { + key_name="$(random_uuid)" + cache_value 'ln_noamount_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-ln-noamount-secret" "$secret" + cache_value "ln-noamount-api-key-id" "$key_id" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":8000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + invoice_response="$(lnd_outside_cli addinvoice)" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + + # Pay 4000 sats to the no-amount invoice + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg payment_request "$payment_request" \ + --argjson amount "4000" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request, amount: $amount}}' + ) + + exec_graphql 'api-key-ln-noamount-secret' 'ln-no-amount-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnNoAmountInvoicePaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'ln_noamount_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "4000" ]] || exit 1 +} + +@test "api-keys-limits: lnNoAmountInvoicePaymentSend exceeding limit fails" { + # Try to pay 5000 more sats (would exceed 8000 limit) + invoice_response="$(lnd_outside_cli addinvoice)" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --arg payment_request "$payment_request" \ + --argjson amount "5000" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request, amount: $amount}}' + ) + + exec_graphql 'api-key-ln-noamount-secret' 'ln-no-amount-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnNoAmountInvoicePaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a daily spending limit restriction + error_code="$(graphql_output '.data.lnNoAmountInvoicePaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.lnNoAmountInvoicePaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: lnNoAmountUsdInvoicePaymentSend respects limits" { + key_name="$(random_uuid)" + cache_value 'ln_noamount_usd_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-ln-noamount-usd-secret" "$secret" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":8000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + invoice_response="$(lnd_outside_cli addinvoice)" + payment_request="$(echo "$invoice_response" | jq -r '.payment_request')" + + # Pay 30 cents (USD) to the no-amount invoice from USD wallet + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.usd_wallet_id)" \ + --arg payment_request "$payment_request" \ + --argjson amount "30" \ + '{input: {walletId: $wallet_id, paymentRequest: $payment_request, amount: $amount}}' + ) + + exec_graphql 'api-key-ln-noamount-usd-secret' 'ln-no-amount-usd-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnNoAmountUsdInvoicePaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'ln_noamount_usd_key_name')'") | .limits.dailySpentSats')" + # USD amount is converted to sats for tracking + [[ "${spent_24h}" -gt "0" ]] || exit 1 +} + +@test "api-keys-limits: lnurlPaymentSend respects limits" { + key_name="$(random_uuid)" + cache_value 'lnurl_key_name' "$key_name" + + variables="{\"input\":{\"name\":\"${key_name}\",\"scopes\":[\"READ\",\"WRITE\"]}}" + exec_graphql 'alice' 'api-key-create' "$variables" + + key="$(graphql_output '.data.apiKeyCreate.apiKey')" + secret="$(graphql_output '.data.apiKeyCreate.apiKeySecret')" + key_id=$(echo "$key" | jq -r '.id') + + cache_value "api-key-lnurl-secret" "$secret" + + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":5000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Send payment via lnurl (to xyz_zap_receiver) + lnurl="lnurl1dp68gup69uhkcmmrv9kxsmmnwsarxvpsxghjuam9d3kz66mwdamkutmvde6hymrs9au8j7jl0fshqhmjv43k26tkv4eq5ndl2y" + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --argjson amount "2000" \ + --arg lnurl "$lnurl" \ + '{input: {walletId: $wallet_id, amount: $amount, lnurl: $lnurl}}' + ) + + exec_graphql 'api-key-lnurl-secret' 'lnurl-payment-send' "$variables" + send_status="$(graphql_output '.data.lnurlPaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'lnurl_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -ge "2000" ]] || exit 1 +} + +@test "api-keys-limits: lnurlPaymentSend exceeding limit fails" { + # Try to send 4000 more sats (would exceed 5000 limit) + lnurl="lnurl1dp68gup69uhkcmmrv9kxsmmnwsarxvpsxghjuam9d3kz66mwdamkutmvde6hymrs9au8j7jl0fshqhmjv43k26tkv4eq5ndl2y" + + variables=$( + jq -n \ + --arg wallet_id "$(read_value $ALICE.btc_wallet_id)" \ + --argjson amount "4000" \ + --arg lnurl "$lnurl" \ + '{input: {walletId: $wallet_id, amount: $amount, lnurl: $lnurl}}' + ) + + exec_graphql 'api-key-lnurl-secret' 'lnurl-payment-send' "$variables" + send_status="$(graphql_output '.data.lnurlPaymentSend.status')" + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error code and message both indicate a daily spending limit restriction + error_code="$(graphql_output '.data.lnurlPaymentSend.errors[0].code')" + [[ "${error_code}" == "TRANSACTION_RESTRICTED" ]] || exit 1 + error_msg="$(graphql_output '.data.lnurlPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} diff --git a/bats/gql/intraledger-payment-send.gql b/bats/gql/intraledger-payment-send.gql index c0e6486715..09ed38d9ca 100644 --- a/bats/gql/intraledger-payment-send.gql +++ b/bats/gql/intraledger-payment-send.gql @@ -2,6 +2,7 @@ mutation intraLedgerPaymentSend($input: IntraLedgerPaymentSendInput!) { intraLedgerPaymentSend(input: $input) { status errors { + code message path } diff --git a/bats/gql/intraledger-usd-payment-send.gql b/bats/gql/intraledger-usd-payment-send.gql index 5e72e9d70a..423d1ef8cd 100644 --- a/bats/gql/intraledger-usd-payment-send.gql +++ b/bats/gql/intraledger-usd-payment-send.gql @@ -2,6 +2,7 @@ mutation intraLedgerUsdPaymentSend($input: IntraLedgerUsdPaymentSendInput!) { intraLedgerUsdPaymentSend(input: $input) { status errors { + code message path } diff --git a/bats/gql/ln-invoice-payment-send.gql b/bats/gql/ln-invoice-payment-send.gql index 8232fb51de..2d08222f30 100644 --- a/bats/gql/ln-invoice-payment-send.gql +++ b/bats/gql/ln-invoice-payment-send.gql @@ -1,6 +1,7 @@ mutation lnInvoicePaymentSend($input: LnInvoicePaymentInput!) { lnInvoicePaymentSend(input: $input) { errors { + code message } status diff --git a/bats/gql/ln-no-amount-invoice-payment-send.gql b/bats/gql/ln-no-amount-invoice-payment-send.gql index 5d647f6b22..4e5abd20e2 100644 --- a/bats/gql/ln-no-amount-invoice-payment-send.gql +++ b/bats/gql/ln-no-amount-invoice-payment-send.gql @@ -1,6 +1,7 @@ mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { lnNoAmountInvoicePaymentSend(input: $input) { errors { + code message } status diff --git a/bats/gql/ln-no-amount-usd-invoice-payment-send.gql b/bats/gql/ln-no-amount-usd-invoice-payment-send.gql index c75aef1ba8..e01473b89d 100644 --- a/bats/gql/ln-no-amount-usd-invoice-payment-send.gql +++ b/bats/gql/ln-no-amount-usd-invoice-payment-send.gql @@ -3,6 +3,7 @@ mutation lnNoAmountUsdInvoicePaymentSend( ) { lnNoAmountUsdInvoicePaymentSend(input: $input) { errors { + code message } status diff --git a/bats/gql/lnurl-payment-send.gql b/bats/gql/lnurl-payment-send.gql index f92ecbf898..ef3292cf70 100644 --- a/bats/gql/lnurl-payment-send.gql +++ b/bats/gql/lnurl-payment-send.gql @@ -1,6 +1,7 @@ mutation lnurlPaymentSend($input: LnurlPaymentSendInput!) { lnurlPaymentSend(input: $input) { errors { + code message path } diff --git a/bats/gql/on-chain-payment-send.gql b/bats/gql/on-chain-payment-send.gql index 097cb6fa4b..818eccdbe3 100644 --- a/bats/gql/on-chain-payment-send.gql +++ b/bats/gql/on-chain-payment-send.gql @@ -1,6 +1,7 @@ mutation onChainPaymentSend($input: OnChainPaymentSendInput!) { onChainPaymentSend(input: $input) { errors { + code message } status diff --git a/core/api-keys/.sqlx/query-484c75fe89364613e436dee47c34376e721165b22f4b49a93ce24a097914e536.json b/core/api-keys/.sqlx/query-484c75fe89364613e436dee47c34376e721165b22f4b49a93ce24a097914e536.json new file mode 100644 index 0000000000..1210fbefc9 --- /dev/null +++ b/core/api-keys/.sqlx/query-484c75fe89364613e436dee47c34376e721165b22f4b49a93ce24a097914e536.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT daily_limit_sats, weekly_limit_sats, monthly_limit_sats, annual_limit_sats\n FROM api_key_limits\n WHERE api_key_id = $1\n FOR UPDATE\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "daily_limit_sats", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "weekly_limit_sats", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "monthly_limit_sats", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "annual_limit_sats", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + true, + true, + true, + true + ] + }, + "hash": "484c75fe89364613e436dee47c34376e721165b22f4b49a93ce24a097914e536" +} diff --git a/core/api-keys/.sqlx/query-54fed150bc898abeb0521a03a99efd4c656219fd7545909263600c3bfdae06bf.json b/core/api-keys/.sqlx/query-54fed150bc898abeb0521a03a99efd4c656219fd7545909263600c3bfdae06bf.json new file mode 100644 index 0000000000..5acc3e4ac4 --- /dev/null +++ b/core/api-keys/.sqlx/query-54fed150bc898abeb0521a03a99efd4c656219fd7545909263600c3bfdae06bf.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT amount_sats\n FROM api_key_transactions\n WHERE transaction_id = $1\n AND api_key_id = $2\n FOR UPDATE\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "amount_sats", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "54fed150bc898abeb0521a03a99efd4c656219fd7545909263600c3bfdae06bf" +} diff --git a/core/api-keys/.sqlx/query-751f836dc8f78c330387456dd68a8803972c7b3e2b6a2b95c27f15068bed2ca5.json b/core/api-keys/.sqlx/query-751f836dc8f78c330387456dd68a8803972c7b3e2b6a2b95c27f15068bed2ca5.json new file mode 100644 index 0000000000..a784094d72 --- /dev/null +++ b/core/api-keys/.sqlx/query-751f836dc8f78c330387456dd68a8803972c7b3e2b6a2b95c27f15068bed2ca5.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pg_advisory_xact_lock", + "type_info": "Void" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "751f836dc8f78c330387456dd68a8803972c7b3e2b6a2b95c27f15068bed2ca5" +} diff --git a/core/api-keys/.sqlx/query-b52c808ef2afc69dcd45ddd6889b5a7ab0b025f6103ce6f0aa95fb12d950d005.json b/core/api-keys/.sqlx/query-b52c808ef2afc69dcd45ddd6889b5a7ab0b025f6103ce6f0aa95fb12d950d005.json deleted file mode 100644 index c499cc970a..0000000000 --- a/core/api-keys/.sqlx/query-b52c808ef2afc69dcd45ddd6889b5a7ab0b025f6103ce6f0aa95fb12d950d005.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "WITH updated_key AS (\n UPDATE identity_api_keys k\n SET last_used_at = NOW(), hashed_key = digest($1, 'sha256')\n FROM identities i\n WHERE k.identity_id = i.id\n AND k.revoked = false\n AND k.encrypted_key = crypt($1, k.encrypted_key)\n AND (k.expires_at > NOW() OR k.expires_at IS NULL)\n RETURNING k.id, i.subject_id, k.scopes\n )\n SELECT id, subject_id, scopes FROM updated_key", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "subject_id", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "scopes", - "type_info": "VarcharArray" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "b52c808ef2afc69dcd45ddd6889b5a7ab0b025f6103ce6f0aa95fb12d950d005" -} diff --git a/core/api-keys/.sqlx/query-e347e89036d70b33d0a1aed6f51bd80036f5c786d0234d451c485f1e12e5794b.json b/core/api-keys/.sqlx/query-e347e89036d70b33d0a1aed6f51bd80036f5c786d0234d451c485f1e12e5794b.json new file mode 100644 index 0000000000..7d5cd41e1a --- /dev/null +++ b/core/api-keys/.sqlx/query-e347e89036d70b33d0a1aed6f51bd80036f5c786d0234d451c485f1e12e5794b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM api_key_transactions\n WHERE transaction_id = $1\n AND api_key_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e347e89036d70b33d0a1aed6f51bd80036f5c786d0234d451c485f1e12e5794b" +} diff --git a/core/api-keys/.sqlx/query-e66c9622a58cb179025b47328b9ac1a47188a5c8b8048a16d51a6e8a4bdc275a.json b/core/api-keys/.sqlx/query-e66c9622a58cb179025b47328b9ac1a47188a5c8b8048a16d51a6e8a4bdc275a.json new file mode 100644 index 0000000000..75d9cf379e --- /dev/null +++ b/core/api-keys/.sqlx/query-e66c9622a58cb179025b47328b9ac1a47188a5c8b8048a16d51a6e8a4bdc275a.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE api_key_transactions\n SET transaction_id = $1\n WHERE transaction_id = $2\n AND api_key_id = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e66c9622a58cb179025b47328b9ac1a47188a5c8b8048a16d51a6e8a4bdc275a" +} diff --git a/core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json b/core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json new file mode 100644 index 0000000000..1908eefb7c --- /dev/null +++ b/core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO api_key_transactions (api_key_id, amount_sats, transaction_id, created_at)\n VALUES ($1, $2, $3, NOW())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a" +} diff --git a/core/api-keys/proto/api_keys.proto b/core/api-keys/proto/api_keys.proto index 6878dc496a..ffd4bd88a6 100644 --- a/core/api-keys/proto/api_keys.proto +++ b/core/api-keys/proto/api_keys.proto @@ -4,6 +4,7 @@ package services.api_keys.v1; service ApiKeysService { rpc CheckSpendingLimit (CheckSpendingLimitRequest) returns (CheckSpendingLimitResponse) {} + rpc CheckAndLockSpending (CheckAndLockSpendingRequest) returns (CheckAndLockSpendingResponse) {} rpc GetSpendingSummary (GetSpendingSummaryRequest) returns (GetSpendingSummaryResponse) {} rpc RecordSpending (RecordSpendingRequest) returns (RecordSpendingResponse) {} rpc ReverseSpending (ReverseSpendingRequest) returns (ReverseSpendingResponse) {} @@ -30,6 +31,15 @@ message CheckSpendingLimitResponse { optional int64 remaining_annual_sats = 13; } +message CheckAndLockSpendingRequest { + string api_key_id = 1; + int64 amount_sats = 2; +} + +message CheckAndLockSpendingResponse { + string ephemeral_id = 1; +} + message GetSpendingSummaryRequest { string api_key_id = 1; } @@ -53,6 +63,7 @@ message RecordSpendingRequest { string api_key_id = 1; int64 amount_sats = 2; optional string transaction_id = 3; + optional string ephemeral_id = 4; } message RecordSpendingResponse { diff --git a/core/api-keys/src/app/mod.rs b/core/api-keys/src/app/mod.rs index e3223fee78..6074e9b435 100644 --- a/core/api-keys/src/app/mod.rs +++ b/core/api-keys/src/app/mod.rs @@ -98,16 +98,29 @@ impl ApiKeysApp { .await?) } + #[tracing::instrument(name = "app.check_and_lock_spending", skip_all)] + pub async fn check_and_lock_spending( + &self, + api_key_id: IdentityApiKeyId, + amount_sats: i64, + ) -> Result { + Ok(self + .limits + .check_and_lock_spending(api_key_id, amount_sats) + .await?) + } + #[tracing::instrument(name = "app.record_spending", skip_all)] pub async fn record_spending( &self, api_key_id: IdentityApiKeyId, amount_sats: i64, transaction_id: Option, + ephemeral_id: Option, ) -> Result<(), ApplicationError> { Ok(self .limits - .record_spending(api_key_id, amount_sats, transaction_id) + .record_spending(api_key_id, amount_sats, transaction_id, ephemeral_id) .await?) } diff --git a/core/api-keys/src/grpc/server/mod.rs b/core/api-keys/src/grpc/server/mod.rs index b8af08d4ee..14ff62b20f 100644 --- a/core/api-keys/src/grpc/server/mod.rs +++ b/core/api-keys/src/grpc/server/mod.rs @@ -11,7 +11,11 @@ use tracing::{grpc, instrument}; use self::proto::{api_keys_service_server::ApiKeysService, *}; use super::config::*; -use crate::{app::ApiKeysApp, identity::IdentityApiKeyId}; +use crate::{ + app::{ApiKeysApp, ApplicationError}, + identity::IdentityApiKeyId, + limits::LimitError, +}; use std::sync::Arc; pub struct ApiKeys { @@ -72,6 +76,39 @@ impl ApiKeysService for ApiKeys { })) } + #[instrument(name = "api_keys.check_and_lock_spending", skip_all, err)] + async fn check_and_lock_spending( + &self, + request: Request, + ) -> Result, Status> { + grpc::extract_tracing(&request); + let request = request.into_inner(); + let CheckAndLockSpendingRequest { + api_key_id, + amount_sats, + } = request; + + let api_key_id = api_key_id + .parse::() + .map_err(|e| Status::invalid_argument(format!("Invalid API key ID: {}", e)))?; + + let ephemeral_id = self + .app + .check_and_lock_spending(api_key_id, amount_sats) + .await + .map_err(|e| match &e { + ApplicationError::Limit(LimitError::LimitExceeded(_)) => { + Status::failed_precondition(e.to_string()) + } + ApplicationError::Limit(LimitError::InvalidLimitAmount) => { + Status::invalid_argument(e.to_string()) + } + _ => Status::internal(e.to_string()), + })?; + + Ok(Response::new(CheckAndLockSpendingResponse { ephemeral_id })) + } + #[instrument(name = "api_keys.get_spending_summary", skip_all, err)] async fn get_spending_summary( &self, @@ -131,6 +168,7 @@ impl ApiKeysService for ApiKeys { api_key_id, amount_sats, transaction_id, + ephemeral_id, } = request; let api_key_id = api_key_id @@ -138,9 +176,22 @@ impl ApiKeysService for ApiKeys { .map_err(|e| Status::invalid_argument(format!("Invalid API key ID: {}", e)))?; self.app - .record_spending(api_key_id, amount_sats, transaction_id) + .record_spending(api_key_id, amount_sats, transaction_id, ephemeral_id) .await - .map_err(|e| Status::internal(e.to_string()))?; + .map_err(|e| match &e { + ApplicationError::Limit(LimitError::InvalidLimitAmount) + | ApplicationError::Limit(LimitError::MissingTransactionId) => { + Status::invalid_argument(e.to_string()) + } + ApplicationError::Limit(LimitError::EphemeralNotFound(_)) => { + Status::not_found(e.to_string()) + } + ApplicationError::Limit(LimitError::AmountMismatch) + | ApplicationError::Limit(LimitError::LimitExceeded(_)) => { + Status::failed_precondition(e.to_string()) + } + _ => Status::internal(e.to_string()), + })?; Ok(Response::new(RecordSpendingResponse {})) } diff --git a/core/api-keys/src/limits/error.rs b/core/api-keys/src/limits/error.rs index c5495ff775..75e8461dc5 100644 --- a/core/api-keys/src/limits/error.rs +++ b/core/api-keys/src/limits/error.rs @@ -7,4 +7,16 @@ pub enum LimitError { #[error("Invalid limit amount (must be positive)")] InvalidLimitAmount, + + #[error("Missing transaction id for ephemeral finalization")] + MissingTransactionId, + + #[error("Spending amount mismatch for transaction reference")] + AmountMismatch, + + #[error("{0} spending limit exceeded")] + LimitExceeded(String), + + #[error("Ephemeral reservation not found: {0}")] + EphemeralNotFound(String), } diff --git a/core/api-keys/src/limits/mod.rs b/core/api-keys/src/limits/mod.rs index b86cae4760..d4f44deeeb 100644 --- a/core/api-keys/src/limits/mod.rs +++ b/core/api-keys/src/limits/mod.rs @@ -57,12 +57,13 @@ impl Limits { Self { pool } } + #[tracing::instrument(name = "limits.check_spending_limit", skip(self))] pub async fn check_spending_limit( &self, api_key_id: IdentityApiKeyId, amount_sats: i64, ) -> Result { - if amount_sats < 0 { + if amount_sats <= 0 { return Err(LimitError::InvalidLimitAmount); } @@ -94,17 +95,169 @@ impl Limits { }) } + #[tracing::instrument(name = "limits.check_and_lock_spending", skip(self))] + pub async fn check_and_lock_spending( + &self, + api_key_id: IdentityApiKeyId, + amount_sats: i64, + ) -> Result { + if amount_sats <= 0 { + return Err(LimitError::InvalidLimitAmount); + } + + let mut tx = self.pool.begin().await?; + + // Acquire a transaction-scoped advisory lock keyed on a stable hash of the + // api_key_id. This serializes concurrent check_and_lock_spending calls for the + // same key, preventing two calls from both reading the spending aggregate before + // either inserts its ephemeral row (the FOR UPDATE on api_key_limits alone cannot + // block concurrent inserts into api_key_transactions when no limits row exists). + sqlx::query!( + "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", + api_key_id.to_string(), + ) + .execute(&mut *tx) + .await?; + + let limits = sqlx::query!( + r#" + SELECT daily_limit_sats, weekly_limit_sats, monthly_limit_sats, annual_limit_sats + FROM api_key_limits + WHERE api_key_id = $1 + FOR UPDATE + "#, + api_key_id as IdentityApiKeyId, + ) + .fetch_optional(&mut *tx) + .await?; + + let (daily_limit, weekly_limit, monthly_limit, annual_limit) = match limits { + Some(row) => ( + row.daily_limit_sats, + row.weekly_limit_sats, + row.monthly_limit_sats, + row.annual_limit_sats, + ), + None => (None, None, None, None), + }; + + let spending = sqlx::query!( + r#" + SELECT + COALESCE(SUM(amount_sats) FILTER (WHERE created_at > NOW() - INTERVAL '24 hours'), 0)::bigint AS "daily_spent_sats!", + COALESCE(SUM(amount_sats) FILTER (WHERE created_at > NOW() - INTERVAL '7 days'), 0)::bigint AS "weekly_spent_sats!", + COALESCE(SUM(amount_sats) FILTER (WHERE created_at > NOW() - INTERVAL '30 days'), 0)::bigint AS "monthly_spent_sats!", + COALESCE(SUM(amount_sats) FILTER (WHERE created_at > NOW() - INTERVAL '365 days'), 0)::bigint AS "annual_spent_sats!" + FROM api_key_transactions + WHERE api_key_id = $1 + AND created_at > NOW() - INTERVAL '365 days' + "#, + api_key_id as IdentityApiKeyId, + ) + .fetch_one(&mut *tx) + .await?; + + let checks = [ + ("daily", daily_limit, spending.daily_spent_sats), + ("weekly", weekly_limit, spending.weekly_spent_sats), + ("monthly", monthly_limit, spending.monthly_spent_sats), + ("annual", annual_limit, spending.annual_spent_sats), + ]; + + for (period, limit, spent) in &checks { + if let Some(limit) = limit { + if spent.saturating_add(amount_sats) > *limit { + return Err(LimitError::LimitExceeded(period.to_string())); + } + } + } + + let ephemeral_id = uuid::Uuid::new_v4().to_string(); + + sqlx::query!( + r#" + INSERT INTO api_key_transactions (api_key_id, amount_sats, transaction_id, created_at) + VALUES ($1, $2, $3, NOW()) + "#, + api_key_id as IdentityApiKeyId, + amount_sats, + &ephemeral_id, + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(ephemeral_id) + } + #[tracing::instrument(name = "limits.record_spending", skip(self))] pub async fn record_spending( &self, api_key_id: IdentityApiKeyId, amount_sats: i64, transaction_id: Option, + ephemeral_id: Option, ) -> Result<(), LimitError> { if amount_sats <= 0 { return Err(LimitError::InvalidLimitAmount); } + let txn_id = transaction_id.ok_or(LimitError::MissingTransactionId)?; + + match ephemeral_id { + Some(eid) => { + self.finalize_ephemeral(api_key_id, amount_sats, txn_id, eid) + .await + } + None => self.record_canonical(api_key_id, amount_sats, txn_id).await, + } + } + + async fn finalize_ephemeral( + &self, + api_key_id: IdentityApiKeyId, + amount_sats: i64, + txn_id: String, + eid: String, + ) -> Result<(), LimitError> { + let mut tx = self.pool.begin().await?; + + let canonical = fetch_transaction(&mut tx, api_key_id, &txn_id).await?; + let ephemeral = fetch_transaction(&mut tx, api_key_id, &eid).await?; + + match (ephemeral, canonical) { + (Some(eph_amount), Some(can_amount)) => { + // Both rows exist: canonical was inserted by a concurrent/prior non-ephemeral call. + // Validate both amounts, then delete the ephemeral row to avoid a UNIQUE violation. + ensure_amount_matches(eph_amount, amount_sats)?; + ensure_amount_matches(can_amount, amount_sats)?; + delete_transaction(&mut tx, api_key_id, &eid).await?; + } + (Some(eph_amount), None) => { + // Happy path: rename the ephemeral row to the final transaction_id. + ensure_amount_matches(eph_amount, amount_sats)?; + rename_transaction(&mut tx, api_key_id, &eid, &txn_id).await?; + } + (None, Some(can_amount)) => { + // Ephemeral row already gone: idempotent retry of a completed finalization. + ensure_amount_matches(can_amount, amount_sats)?; + } + (None, None) => { + return Err(LimitError::EphemeralNotFound(eid)); + } + } + + tx.commit().await?; + Ok(()) + } + + async fn record_canonical( + &self, + api_key_id: IdentityApiKeyId, + amount_sats: i64, + txn_id: String, + ) -> Result<(), LimitError> { sqlx::query!( r#" INSERT INTO api_key_transactions (api_key_id, amount_sats, transaction_id, created_at) @@ -113,7 +266,7 @@ impl Limits { "#, api_key_id as IdentityApiKeyId, amount_sats, - transaction_id, + txn_id, ) .execute(&self.pool) .await?; @@ -431,6 +584,78 @@ impl Limits { } } +fn ensure_amount_matches(stored: i64, expected: i64) -> Result<(), LimitError> { + if stored != expected { + return Err(LimitError::AmountMismatch); + } + Ok(()) +} + +async fn fetch_transaction( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + api_key_id: IdentityApiKeyId, + transaction_id: &str, +) -> Result, LimitError> { + let row = sqlx::query!( + r#" + SELECT amount_sats + FROM api_key_transactions + WHERE transaction_id = $1 + AND api_key_id = $2 + FOR UPDATE + "#, + transaction_id, + api_key_id as IdentityApiKeyId, + ) + .fetch_optional(&mut **tx) + .await?; + + Ok(row.map(|r| r.amount_sats)) +} + +async fn delete_transaction( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + api_key_id: IdentityApiKeyId, + transaction_id: &str, +) -> Result<(), LimitError> { + sqlx::query!( + r#" + DELETE FROM api_key_transactions + WHERE transaction_id = $1 + AND api_key_id = $2 + "#, + transaction_id, + api_key_id as IdentityApiKeyId, + ) + .execute(&mut **tx) + .await?; + + Ok(()) +} + +async fn rename_transaction( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + api_key_id: IdentityApiKeyId, + from_id: &str, + to_id: &str, +) -> Result<(), LimitError> { + sqlx::query!( + r#" + UPDATE api_key_transactions + SET transaction_id = $1 + WHERE transaction_id = $2 + AND api_key_id = $3 + "#, + to_id, + from_id, + api_key_id as IdentityApiKeyId, + ) + .execute(&mut **tx) + .await?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -454,17 +679,28 @@ mod tests { assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); } + #[tokio::test] + async fn check_spending_limit_rejects_zero_amount() { + let limits = test_limits(); + let result = limits.check_spending_limit(test_api_key_id(), 0).await; + assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); + } + #[tokio::test] async fn record_spending_rejects_zero_amount() { let limits = test_limits(); - let result = limits.record_spending(test_api_key_id(), 0, None).await; + let result = limits + .record_spending(test_api_key_id(), 0, None, None) + .await; assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); } #[tokio::test] async fn record_spending_rejects_negative_amount() { let limits = test_limits(); - let result = limits.record_spending(test_api_key_id(), -100, None).await; + let result = limits + .record_spending(test_api_key_id(), -100, None, None) + .await; assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); } @@ -523,4 +759,63 @@ mod tests { let result = limits.set_annual_limit(test_api_key_id(), -1).await; assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); } + + #[tokio::test] + async fn check_and_lock_spending_rejects_zero_amount() { + let limits = test_limits(); + let result = limits.check_and_lock_spending(test_api_key_id(), 0).await; + assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); + } + + #[tokio::test] + async fn check_and_lock_spending_rejects_negative_amount() { + let limits = test_limits(); + let result = limits.check_and_lock_spending(test_api_key_id(), -1).await; + assert!(matches!(result, Err(LimitError::InvalidLimitAmount))); + } + + #[tokio::test] + async fn record_spending_requires_transaction_id_with_ephemeral_id() { + let limits = test_limits(); + let result = limits + .record_spending( + test_api_key_id(), + 1000, + None, + Some("ephemeral-123".to_string()), + ) + .await; + assert!(matches!(result, Err(LimitError::MissingTransactionId))); + } + + #[tokio::test] + async fn record_spending_without_ephemeral_id_requires_transaction_id() { + let limits = test_limits(); + let result = limits + .record_spending(test_api_key_id(), 1000, None, None) + .await; + assert!(matches!(result, Err(LimitError::MissingTransactionId))); + } + + #[test] + fn ensure_amount_matches_returns_ok_when_amounts_match() { + assert!(ensure_amount_matches(1000, 1000).is_ok()); + } + + #[test] + fn ensure_amount_matches_returns_ok_for_zero() { + assert!(ensure_amount_matches(0, 0).is_ok()); + } + + #[test] + fn ensure_amount_matches_returns_error_when_stored_is_less() { + let result = ensure_amount_matches(500, 1000); + assert!(matches!(result, Err(LimitError::AmountMismatch))); + } + + #[test] + fn ensure_amount_matches_returns_error_when_stored_is_more() { + let result = ensure_amount_matches(1000, 500); + assert!(matches!(result, Err(LimitError::AmountMismatch))); + } } diff --git a/core/api/package.json b/core/api/package.json index 48f5303806..4620a7039d 100644 --- a/core/api/package.json +++ b/core/api/package.json @@ -5,7 +5,7 @@ "eslint-check": "eslint src test --ext .ts", "eslint-fix": "eslint src test --ext .ts --fix", "circular-deps-check": "madge --circular --extensions ts src", - "build": "tsc -p tsconfig-build.json && cp -R src/services/price/protos dist/services/price/ && cp -R src/services/dealer-price/proto dist/services/dealer-price/ && cp -R src/services/bria/proto dist/services/bria/ && cp -R src/services/notifications/proto dist/services/notifications/ && tscpaths --silent -p tsconfig.json -s ./src -o ./dist", + "build": "tsc -p tsconfig-build.json && cp -R src/services/price/protos dist/services/price/ && cp -R src/services/dealer-price/proto dist/services/dealer-price/ && cp -R src/services/bria/proto dist/services/bria/ && cp -R src/services/notifications/proto dist/services/notifications/ && cp -R src/services/api-keys/proto dist/services/api-keys/ && tscpaths --silent -p tsconfig.json -s ./src -o ./dist", "trigger": "pnpm run build && node dist/servers/trigger.js | pino-pretty -c -l", "ws": "pnpm run build && node dist/servers/ws-server.js | pino-pretty -c -l", "watch": "nodemon -V -e ts,graphql -w ./src -x pnpm run start", @@ -32,7 +32,8 @@ "migrate:up": "migrate-mongo up -f src/migrations/migrate-mongo-config.js", "migrate:down": "migrate-mongo down -f src/migrations/migrate-mongo-config.js", "mongodb-migrate": "pnpm run migrate:status && pnpm run migrate:up && pnpm run migrate:status", - "codegen:notifications": "cd ./src/services/notifications/proto && buf generate" + "codegen:notifications": "cd ./src/services/notifications/proto && buf generate", + "codegen:api-keys": "cd ./src/services/api-keys/proto && buf generate" }, "engines": { "node": "20" @@ -64,7 +65,7 @@ "@prelude.so/sdk": "^0.7.0", "@t3-oss/env-core": "^0.7.3", "ajv": "^8.17.1", - "axios": "^1.11.0", + "axios": "^1.15.0", "axios-retry": "^4.5.0", "basic-auth": "^2.0.1", "bignumber.js": "^9.3.1", diff --git a/core/api/src/app/errors.ts b/core/api/src/app/errors.ts index 9eac8c0bf2..c8b7a4a51b 100644 --- a/core/api/src/app/errors.ts +++ b/core/api/src/app/errors.ts @@ -25,6 +25,7 @@ import * as WalletInvoiceErrors from "@/domain/wallet-invoices/errors" import * as SupportError from "@/domain/support/errors" import * as OathkeeperError from "@/domain/oathkeeper/errors" import * as KratosErrors from "@/domain/kratos/errors" +import * as ApiKeysErrors from "@/domain/api-keys/errors" import * as LedgerFacadeErrors from "@/services/ledger/domain/errors" import * as BriaEventErrors from "@/services/bria/errors" @@ -58,6 +59,7 @@ export const ApplicationErrors = { ...SupportError, ...OathkeeperError, ...KratosErrors, + ...ApiKeysErrors, ...LedgerFacadeErrors, ...BriaEventErrors, diff --git a/core/api/src/app/payments/api-key-spending.ts b/core/api/src/app/payments/api-key-spending.ts new file mode 100644 index 0000000000..a9f1a58672 --- /dev/null +++ b/core/api/src/app/payments/api-key-spending.ts @@ -0,0 +1,70 @@ +import { ApiKeysService } from "@/services/api-keys" + +export const ApiKeySpendingSettlementType = { + Record: "record", + Reverse: "reverse", +} as const + +export const recordApiKeySpendingSettlement = ( + transactionId: LedgerJournalId, +): ApiKeySpendingSettlement => ({ + type: ApiKeySpendingSettlementType.Record, + transactionId, +}) + +export const reverseApiKeySpendingSettlement = (): ApiKeySpendingSettlement => ({ + type: ApiKeySpendingSettlementType.Reverse, +}) + +export const lockApiKeySpending = async ({ + apiKeyId, + amount, +}: { + apiKeyId?: ApiKeyId + amount: BtcPaymentAmount +}): Promise => { + if (!apiKeyId) return undefined + + const apiKeys = ApiKeysService() + const ephemeralId = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount, + }) + if (ephemeralId instanceof Error) return ephemeralId + + return { + apiKeyId, + amount, + ephemeralId, + } +} + +export const settleApiKeySpending = async ({ + lock, + settlement, +}: { + lock?: ApiKeySpendingLock + settlement: ApiKeySpendingSettlement +}): Promise => { + if (!lock) return true + + const apiKeys = ApiKeysService() + + if (settlement.type === ApiKeySpendingSettlementType.Reverse) { + const reverseResult = await apiKeys.reverseSpending({ + transactionId: lock.ephemeralId, + }) + if (reverseResult instanceof Error) return reverseResult + return true + } + + const recordResult = await apiKeys.recordSpending({ + apiKeyId: lock.apiKeyId, + amount: lock.amount, + transactionId: settlement.transactionId, + ephemeralId: lock.ephemeralId, + }) + if (recordResult instanceof Error) return recordResult + + return true +} diff --git a/core/api/src/app/payments/index.types.d.ts b/core/api/src/app/payments/index.types.d.ts index e6ad22b814..aa2e4caf4e 100644 --- a/core/api/src/app/payments/index.types.d.ts +++ b/core/api/src/app/payments/index.types.d.ts @@ -3,6 +3,39 @@ type PaymentSendResult = { transaction: WalletTransaction } +type ApiKeySpendingLock = { + apiKeyId: ApiKeyId + amount: BtcPaymentAmount + ephemeralId: EphemeralId +} + +type ApiKeySpendingSettlementType = "record" | "reverse" + +type ApiKeySpendingSettlement = + | { + type: "record" + transactionId: LedgerJournalId + } + | { + type: "reverse" + } + +// NOTE: api-key settlement behavior must be explicit and must not be inferred +// from result shape/status (e.g. result instanceof Error). +type ApiKeySpendingSettlementTypeObj = + typeof import("./api-key-spending").ApiKeySpendingSettlementType + +type SpendingLimitsExecutionResult = + | { + apiKeySettlement: ApiKeySpendingSettlementTypeObj["Record"] + settlementTransactionId: LedgerJournalId + result: PaymentSendResult | ApplicationError + } + | { + apiKeySettlement: ApiKeySpendingSettlementTypeObj["Reverse"] + result: PaymentSendResult | ApplicationError + } + type PaymentSendAttemptResultTypeObj = typeof import("./ln-send-result").PaymentSendAttemptResultType type PaymentSendAttemptResultType = diff --git a/core/api/src/app/payments/send-intraledger.ts b/core/api/src/app/payments/send-intraledger.ts index af7a768dfa..d4736b1dbc 100644 --- a/core/api/src/app/payments/send-intraledger.ts +++ b/core/api/src/app/payments/send-intraledger.ts @@ -1,12 +1,13 @@ import { getPriceRatioForLimits } from "./helpers" +import { + recordSettlement, + reverseSettlement, + withSpendingLimits, +} from "./spending-limits" import { getValuesToSkipProbe } from "@/config" -import { - checkIntraledgerLimits, - checkTradeIntraAccountLimits, - createIntraledgerContact, -} from "@/app/accounts" +import { createIntraledgerContact } from "@/app/accounts" import { btcFromUsdMidPriceFn, getCurrentPriceAsDisplayPriceRatio, @@ -53,6 +54,7 @@ const intraledgerPaymentSendWalletId = async ({ amount: uncheckedAmount, memo, senderWalletId: uncheckedSenderWalletId, + apiKeyId, }: IntraLedgerPaymentSendWalletIdArgs): Promise => { const validatedPaymentInputs = await validateIntraledgerPaymentInputs({ uncheckedSenderWalletId, @@ -128,7 +130,9 @@ const intraledgerPaymentSendWalletId = async ({ recipientUser, senderUser, memo, + apiKeyId, }) + if (paymentSendResult instanceof Error) return paymentSendResult if (senderAccount.id !== recipientAccount.id) { @@ -231,6 +235,7 @@ const executePaymentViaIntraledger = async < recipientUser, senderUser, memo, + apiKeyId, }: { paymentFlow: PaymentFlow senderAccount: Account @@ -239,6 +244,7 @@ const executePaymentViaIntraledger = async < recipientUser: User senderUser: User memo: string | null + apiKeyId?: ApiKeyId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, @@ -247,17 +253,6 @@ const executePaymentViaIntraledger = async < const priceRatioForLimits = await getPriceRatioForLimits(paymentFlow.paymentAmounts()) if (priceRatioForLimits instanceof Error) return priceRatioForLimits - const checkLimits = - senderAccount.id === recipientAccount.id - ? checkTradeIntraAccountLimits - : checkIntraledgerLimits - const limitCheck = await checkLimits({ - amount: paymentFlow.usdPaymentAmount, - accountId: senderAccount.id, - priceRatio: priceRatioForLimits, - }) - if (limitCheck instanceof Error) return limitCheck - const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() if (!recipientWalletDescriptor) { return new InvalidLightningPaymentFlowBuilderStateError( @@ -283,45 +278,74 @@ const executePaymentViaIntraledger = async < phoneNumber: senderUser.phone, } - const journalId = await LockService().lockWalletId(senderWalletId, async (signal) => - lockedPaymentViaIntraledgerSteps({ - signal, - - paymentFlow, - senderDisplayCurrency: senderAccount.displayCurrency, - senderUsername: senderAccount.username, - recipientDisplayCurrency: recipientAccount.displayCurrency, - recipientUsername: recipientAccount.username, - - memo, - }), - ) - if (journalId instanceof Error) return journalId - - const recipientWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: recipientWalletDescriptor.id, - journalId, - }) - if (recipientWalletTransaction instanceof Error) return recipientWalletTransaction - NotificationsService().sendTransaction({ - recipient: recipientAsNotificationRecipient, - transaction: recipientWalletTransaction, - }) - - const senderWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: senderWalletId, - journalId, - }) - if (senderWalletTransaction instanceof Error) return senderWalletTransaction - NotificationsService().sendTransaction({ - recipient: senderAsNotificationRecipient, - transaction: senderWalletTransaction, + const paymentSendResult = await withSpendingLimits({ + settlementMethod: SettlementMethod.IntraLedger, + accountId: senderAccount.id, + recipientAccountId: recipientAccount.id, + usdPaymentAmount: paymentFlow.usdPaymentAmount, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount: paymentFlow.btcPaymentAmount, + execute: async () => { + const journalId = await LockService().lockWalletId(senderWalletId, async (signal) => + lockedPaymentViaIntraledgerSteps({ + signal, + + paymentFlow, + senderDisplayCurrency: senderAccount.displayCurrency, + senderUsername: senderAccount.username, + recipientDisplayCurrency: recipientAccount.displayCurrency, + recipientUsername: recipientAccount.username, + + memo, + }), + ) + if (journalId instanceof Error) { + return reverseSettlement({ result: journalId }) + } + + const recipientWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: recipientWalletDescriptor.id, + journalId, + }) + if (recipientWalletTransaction instanceof Error) { + return recordSettlement({ + result: recipientWalletTransaction, + settlementTransactionId: journalId, + }) + } + + NotificationsService().sendTransaction({ + recipient: recipientAsNotificationRecipient, + transaction: recipientWalletTransaction, + }) + + const senderWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletId, + journalId, + }) + if (senderWalletTransaction instanceof Error) { + return recordSettlement({ + result: senderWalletTransaction, + settlementTransactionId: journalId, + }) + } + + NotificationsService().sendTransaction({ + recipient: senderAsNotificationRecipient, + transaction: senderWalletTransaction, + }) + + return recordSettlement({ + result: { + status: PaymentSendStatus.Success, + transaction: senderWalletTransaction, + }, + settlementTransactionId: journalId, + }) + }, }) - - return { - status: PaymentSendStatus.Success, - transaction: senderWalletTransaction, - } + return paymentSendResult } const lockedPaymentViaIntraledgerSteps = async ({ diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 57dea4ad31..70974f2efe 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -5,7 +5,11 @@ import { LnSendAttemptResult, PaymentSendAttemptResultType, } from "./ln-send-result" - +import { + recordSettlement, + reverseSettlement, + withSpendingLimits, +} from "./spending-limits" import { reimburseFee } from "./reimburse-fee" import { AccountValidator } from "@/domain/accounts" @@ -61,12 +65,7 @@ import { recordExceptionInCurrentSpan, } from "@/services/tracing" -import { - checkIntraledgerLimits, - checkTradeIntraAccountLimits, - checkWithdrawalLimits, - createIntraledgerContact, -} from "@/app/accounts" +import { createIntraledgerContact } from "@/app/accounts" import { getCurrentPriceAsDisplayPriceRatio } from "@/app/prices" import { getTransactionForWalletByJournalId, @@ -80,11 +79,56 @@ import { ResourceExpiredLockServiceError } from "@/domain/lock" const dealer = DealerPriceService() const paymentFlowRepo = PaymentFlowStateRepository(defaultTimeToExpiryInSeconds) +const getValidatedIntraledgerRecipientAccount = async < + S extends WalletCurrency, + R extends WalletCurrency, +>({ + paymentFlow, +}: { + paymentFlow: PaymentFlow +}): Promise => { + const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() + if (!recipientWalletDescriptor) { + return new InvalidLightningPaymentFlowBuilderStateError( + "Expected recipient details missing", + ) + } + + const recipientAccount = await AccountsRepository().findById( + recipientWalletDescriptor.accountId, + ) + if (recipientAccount instanceof Error) return recipientAccount + + const accountValidator = AccountValidator(recipientAccount) + if (accountValidator instanceof Error) return accountValidator + + return recipientAccount +} + +const addIntraledgerContactIfNeeded = async ({ + senderAccount, + recipientAccount, +}: { + senderAccount: Account + recipientAccount: Account +}): Promise => { + if (senderAccount.id === recipientAccount.id) return + + const addContactResult = await createIntraledgerContact({ + senderAccount, + recipientAccount, + }) + if (addContactResult instanceof Error) { + recordExceptionInCurrentSpan({ error: addContactResult, level: ErrorLevel.Warn }) + } +} + export const payInvoiceByWalletId = async ({ uncheckedPaymentRequest, memo, senderWalletId: uncheckedSenderWalletId, senderAccount, + apiKeyId, }: PayInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, @@ -121,41 +165,24 @@ export const payInvoiceByWalletId = async ({ paymentFlow, senderAccount, memo, + apiKeyId, }) } - const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() - if (!recipientWalletDescriptor) { - return new InvalidLightningPaymentFlowBuilderStateError( - "Expected recipient details missing", - ) - } - const recipientAccount = await AccountsRepository().findById( - recipientWalletDescriptor.accountId, - ) + const recipientAccount = await getValidatedIntraledgerRecipientAccount({ paymentFlow }) if (recipientAccount instanceof Error) return recipientAccount - const accountValidator = AccountValidator(recipientAccount) - if (accountValidator instanceof Error) return accountValidator - const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderWalletId, senderAccount, recipientAccount, memo, + apiKeyId, }) if (paymentSendResult instanceof Error) return paymentSendResult - if (senderAccount.id !== recipientAccount.id) { - const addContactResult = await createIntraledgerContact({ - senderAccount, - recipientAccount, - }) - if (addContactResult instanceof Error) { - recordExceptionInCurrentSpan({ error: addContactResult, level: ErrorLevel.Warn }) - } - } + await addIntraledgerContactIfNeeded({ senderAccount, recipientAccount }) return paymentSendResult } @@ -166,6 +193,7 @@ const payNoAmountInvoiceByWalletId = async ({ memo, senderWalletId: uncheckedSenderWalletId, senderAccount, + apiKeyId, }: PayNoAmountInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, @@ -204,42 +232,24 @@ const payNoAmountInvoiceByWalletId = async ({ paymentFlow, senderAccount, memo, + apiKeyId, }) } - const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() - if (!recipientWalletDescriptor) { - return new InvalidLightningPaymentFlowBuilderStateError( - "Expected recipient details missing", - ) - } - - const recipientAccount = await AccountsRepository().findById( - recipientWalletDescriptor.accountId, - ) + const recipientAccount = await getValidatedIntraledgerRecipientAccount({ paymentFlow }) if (recipientAccount instanceof Error) return recipientAccount - const accountValidator = AccountValidator(recipientAccount) - if (accountValidator instanceof Error) return accountValidator - const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderWalletId, senderAccount, recipientAccount, memo, + apiKeyId, }) if (paymentSendResult instanceof Error) return paymentSendResult - if (senderAccount.id !== recipientAccount.id) { - const addContactResult = await createIntraledgerContact({ - senderAccount, - recipientAccount, - }) - if (addContactResult instanceof Error) { - recordExceptionInCurrentSpan({ error: addContactResult, level: ErrorLevel.Warn }) - } - } + await addIntraledgerContactIfNeeded({ senderAccount, recipientAccount }) return paymentSendResult } @@ -432,12 +442,14 @@ const executePaymentViaIntraledger = async < senderWalletId, recipientAccount, memo, + apiKeyId, }: { paymentFlow: PaymentFlow senderAccount: Account senderWalletId: WalletId recipientAccount: Account memo: string | null + apiKeyId?: ApiKeyId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, @@ -449,17 +461,6 @@ const executePaymentViaIntraledger = async < const paymentHash = paymentFlow.paymentHashForFlow() if (paymentHash instanceof Error) return paymentHash - const checkLimits = - senderAccount.id === recipientAccount.id - ? checkTradeIntraAccountLimits - : checkIntraledgerLimits - const limitCheck = await checkLimits({ - amount: paymentFlow.usdPaymentAmount, - accountId: senderAccount.id, - priceRatio: priceRatioForLimits, - }) - if (limitCheck instanceof Error) return limitCheck - const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() if (!recipientWalletDescriptor) { return new InvalidLightningPaymentFlowBuilderStateError( @@ -491,65 +492,90 @@ const executePaymentViaIntraledger = async < phoneNumber: senderUser.phone, } - const paymentSendAttemptResult = await LockService().lockWalletId( - senderWalletId, - async (signal) => - lockedPaymentViaIntraledgerSteps({ - signal, - - paymentHash, - paymentFlow, - senderDisplayCurrency: senderAccount.displayCurrency, - senderUsername: senderAccount.username, - recipientDisplayCurrency: recipientAccount.displayCurrency, - recipientUsername: recipientAccount.username, - - memo, - }), - ) - if (paymentSendAttemptResult instanceof Error) return paymentSendAttemptResult + const paymentSendResult = await withSpendingLimits({ + settlementMethod: SettlementMethod.IntraLedger, + accountId: senderAccount.id, + recipientAccountId: recipientAccount.id, + usdPaymentAmount: paymentFlow.usdPaymentAmount, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount: paymentFlow.btcPaymentAmount, + execute: async () => { + const paymentSendAttemptResult = await LockService().lockWalletId( + senderWalletId, + async (signal) => + lockedPaymentViaIntraledgerSteps({ + signal, + + paymentHash, + paymentFlow, + senderDisplayCurrency: senderAccount.displayCurrency, + senderUsername: senderAccount.username, + recipientDisplayCurrency: recipientAccount.displayCurrency, + recipientUsername: recipientAccount.username, + + memo, + }), + ) + if (paymentSendAttemptResult instanceof Error) { + return reverseSettlement({ result: paymentSendAttemptResult }) + } - switch (paymentSendAttemptResult.type) { - case PaymentSendAttemptResultType.Error: - return paymentSendAttemptResult.error + switch (paymentSendAttemptResult.type) { + case PaymentSendAttemptResultType.Error: + return reverseSettlement({ result: paymentSendAttemptResult.error }) - case PaymentSendAttemptResultType.AlreadyPaid: - return getAlreadyPaidResponse({ - walletId: senderWalletId, - paymentHash, - }) - } + case PaymentSendAttemptResultType.AlreadyPaid: { + const result = await getAlreadyPaidResponse({ + walletId: senderWalletId, + paymentHash, + }) + return reverseSettlement({ result }) + } + } - const { journalId } = paymentSendAttemptResult + const { journalId } = paymentSendAttemptResult - const recipientWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: recipientWalletDescriptor.id, - journalId, - }) - if (recipientWalletTransaction instanceof Error) { - return recipientWalletTransaction - } - NotificationsService().sendTransaction({ - recipient: recipientAsNotificationRecipient, - transaction: recipientWalletTransaction, - }) + const recipientWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: recipientWalletDescriptor.id, + journalId, + }) + if (recipientWalletTransaction instanceof Error) { + return recordSettlement({ + result: recipientWalletTransaction, + settlementTransactionId: journalId, + }) + } + NotificationsService().sendTransaction({ + recipient: recipientAsNotificationRecipient, + transaction: recipientWalletTransaction, + }) - const senderWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: senderWalletId, - journalId, - }) - if (senderWalletTransaction instanceof Error) { - return senderWalletTransaction - } - NotificationsService().sendTransaction({ - recipient: senderAsNotificationRecipient, - transaction: senderWalletTransaction, - }) + const senderWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletId, + journalId, + }) + if (senderWalletTransaction instanceof Error) { + return recordSettlement({ + result: senderWalletTransaction, + settlementTransactionId: journalId, + }) + } + NotificationsService().sendTransaction({ + recipient: senderAsNotificationRecipient, + transaction: senderWalletTransaction, + }) - return { - status: PaymentSendStatus.Success, - transaction: senderWalletTransaction, - } + return recordSettlement({ + result: { + status: PaymentSendStatus.Success, + transaction: senderWalletTransaction, + }, + settlementTransactionId: journalId, + }) + }, + }) + return paymentSendResult } const lockedPaymentViaIntraledgerSteps = async ({ @@ -727,11 +753,13 @@ const executePaymentViaLn = async ({ paymentFlow, senderAccount, memo, + apiKeyId, }: { decodedInvoice: LnInvoice paymentFlow: PaymentFlow senderAccount: Account memo: string | null + apiKeyId?: ApiKeyId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.Lightning, @@ -742,13 +770,6 @@ const executePaymentViaLn = async ({ const priceRatioForLimits = await getPriceRatioForLimits(paymentFlow.paymentAmounts()) if (priceRatioForLimits instanceof Error) return priceRatioForLimits - const limitCheck = await checkWithdrawalLimits({ - amount: paymentFlow.usdPaymentAmount, - accountId: senderAccount.id, - priceRatio: priceRatioForLimits, - }) - if (limitCheck instanceof Error) return limitCheck - const accountWalletDescriptors = await WalletsRepository().findAccountWalletsByAccountId(senderAccount.id) if (accountWalletDescriptors instanceof Error) return accountWalletDescriptors @@ -766,58 +787,94 @@ const executePaymentViaLn = async ({ phoneNumber: senderUser.phone, } - const paymentSendAttemptResult = await LockService().lockWalletId( - senderWalletId, - (signal) => - lockedPaymentViaLnSteps({ - signal, - - decodedInvoice, - paymentFlow, - senderDisplayCurrency: senderAccount.displayCurrency, - memo, - - walletIds, - }), - ) - if (paymentSendAttemptResult instanceof Error) return paymentSendAttemptResult - if (paymentSendAttemptResult.type === PaymentSendAttemptResultType.Error) { - return paymentSendAttemptResult.error - } + const paymentSendResult = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: senderAccount.id, + usdPaymentAmount: paymentFlow.usdPaymentAmount, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount: paymentFlow.btcPaymentAmount, + execute: async () => { + const paymentSendAttemptResult = await LockService().lockWalletId( + senderWalletId, + (signal) => + lockedPaymentViaLnSteps({ + signal, - const walletTransaction = await getTransactionForWalletByJournalId({ - walletId: senderWalletId, - journalId: paymentSendAttemptResult.journalId, - }) - if (walletTransaction instanceof Error) return walletTransaction - NotificationsService().sendTransaction({ - recipient: notificationRecipient, - transaction: walletTransaction, - }) + decodedInvoice, + paymentFlow, + senderDisplayCurrency: senderAccount.displayCurrency, + memo, + + walletIds, + }), + ) + if (paymentSendAttemptResult instanceof Error) { + return reverseSettlement({ result: paymentSendAttemptResult }) + } + if (paymentSendAttemptResult.type === PaymentSendAttemptResultType.Error) { + return reverseSettlement({ result: paymentSendAttemptResult.error }) + } - const { paymentHash } = decodedInvoice - switch (paymentSendAttemptResult.type) { - case PaymentSendAttemptResultType.ErrorWithJournal: - return paymentSendAttemptResult.error + if (paymentSendAttemptResult.type === PaymentSendAttemptResultType.AlreadyPaid) { + const { paymentHash } = decodedInvoice + const result = await getAlreadyPaidResponse({ + walletId: senderWalletId, + paymentHash, + }) + return reverseSettlement({ result }) + } - case PaymentSendAttemptResultType.Pending: - return getPendingPaymentResponse({ + const walletTransaction = await getTransactionForWalletByJournalId({ walletId: senderWalletId, - paymentHash, + journalId: paymentSendAttemptResult.journalId, }) + if (walletTransaction instanceof Error) { + if ( + paymentSendAttemptResult.type === PaymentSendAttemptResultType.ErrorWithJournal + ) { + return reverseSettlement({ result: walletTransaction }) + } + return recordSettlement({ + result: walletTransaction, + settlementTransactionId: paymentSendAttemptResult.journalId, + }) + } - case PaymentSendAttemptResultType.AlreadyPaid: - return getAlreadyPaidResponse({ - walletId: senderWalletId, - paymentHash, + NotificationsService().sendTransaction({ + recipient: notificationRecipient, + transaction: walletTransaction, }) - default: - return { - status: PaymentSendStatus.Success, - transaction: walletTransaction, + const { paymentHash } = decodedInvoice + switch (paymentSendAttemptResult.type) { + case PaymentSendAttemptResultType.ErrorWithJournal: + return reverseSettlement({ result: paymentSendAttemptResult.error }) + + case PaymentSendAttemptResultType.Pending: { + const result = await getPendingPaymentResponse({ + walletId: senderWalletId, + paymentHash, + }) + + return recordSettlement({ + result, + settlementTransactionId: paymentSendAttemptResult.journalId, + }) + } + + default: + return recordSettlement({ + result: { + status: PaymentSendStatus.Success, + transaction: walletTransaction, + }, + settlementTransactionId: paymentSendAttemptResult.journalId, + }) } - } + }, + }) + return paymentSendResult } const lockedPaymentViaLnSteps = async ({ diff --git a/core/api/src/app/payments/send-lnurl.ts b/core/api/src/app/payments/send-lnurl.ts index 1c9168483e..3372395db4 100644 --- a/core/api/src/app/payments/send-lnurl.ts +++ b/core/api/src/app/payments/send-lnurl.ts @@ -8,6 +8,7 @@ export const lnAddressPaymentSend = async ({ senderAccount, amount: uncheckedAmount, lnAddress, + apiKeyId, }: LnAddressPaymentSendArgs): Promise => { const amount = checkedToBtcPaymentAmount(uncheckedAmount) @@ -29,6 +30,7 @@ export const lnAddressPaymentSend = async ({ memo: null, senderWalletId, senderAccount, + apiKeyId, }) } @@ -37,6 +39,7 @@ export const lnurlPaymentSend = async ({ senderAccount, amount: uncheckedAmount, lnurl, + apiKeyId, }: LnurlPaymentSendArgs): Promise => { const amount = checkedToBtcPaymentAmount(uncheckedAmount) @@ -58,5 +61,6 @@ export const lnurlPaymentSend = async ({ memo: null, senderWalletId, senderAccount, + apiKeyId, }) } diff --git a/core/api/src/app/payments/send-on-chain.ts b/core/api/src/app/payments/send-on-chain.ts index a648dba63a..9bd6c35e45 100644 --- a/core/api/src/app/payments/send-on-chain.ts +++ b/core/api/src/app/payments/send-on-chain.ts @@ -1,12 +1,11 @@ import { getPriceRatioForLimits } from "./helpers" +import { + recordSettlement, + reverseSettlement, + withSpendingLimits, +} from "./spending-limits" import { NETWORK, getOnChainWalletConfig } from "@/config" - -import { - checkIntraledgerLimits, - checkTradeIntraAccountLimits, - checkWithdrawalLimits, -} from "@/app/accounts" import { btcFromUsdMidPriceFn, getCurrentPriceAsDisplayPriceRatio, @@ -65,6 +64,7 @@ const payOnChainByWalletId = async ({ speed, memo, sendAll, + apiKeyId, }: PayOnChainByWalletIdArgs): Promise => { const latestAccountState = await AccountsRepository().findById(senderAccount.id) if (latestAccountState instanceof Error) return latestAccountState @@ -178,6 +178,7 @@ const payOnChainByWalletId = async ({ senderAccount, memo, sendAll, + apiKeyId, }) } @@ -193,6 +194,7 @@ const payOnChainByWalletId = async ({ memo, sendAll, logger: onchainLogger, + apiKeyId, }) } @@ -248,11 +250,13 @@ const executePaymentViaIntraledger = async < senderAccount, memo, sendAll, + apiKeyId, }: { builder: OPFBWithConversion | OPFBWithError senderAccount: Account memo: string | null sendAll: boolean + apiKeyId?: ApiKeyId }): Promise => { const paymentFlow = await builder.withoutMinerFee() if (paymentFlow instanceof Error) return paymentFlow @@ -289,78 +293,94 @@ const executePaymentViaIntraledger = async < const priceRatioForLimits = await getPriceRatioForLimits(paymentFlow.paymentAmounts()) if (priceRatioForLimits instanceof Error) return priceRatioForLimits - const checkLimits = - senderAccount.id === recipientAccount.id - ? checkTradeIntraAccountLimits - : checkIntraledgerLimits - const limitCheck = await checkLimits({ - amount: paymentFlow.usdPaymentAmount, + const paymentSendResult = await withSpendingLimits({ + settlementMethod: SettlementMethod.IntraLedger, accountId: senderAccount.id, - priceRatio: priceRatioForLimits, - }) - if (limitCheck instanceof Error) return limitCheck - - const journalId = await LockService().lockWalletId(senderWalletId, async (signal) => - lockedPaymentViaIntraledgerSteps({ - signal, - - paymentFlow, - senderDisplayCurrency: senderAccount.displayCurrency, - senderUsername: senderAccount.username, - recipientDisplayCurrency: recipientAccount.displayCurrency, - recipientUsername: recipientAccount.username, - - memo, - sendAll, - }), - ) - if (journalId instanceof Error) return journalId - - const recipientAsNotificationRecipient = { - accountId: recipientAccount.id, - walletId: recipientWalletDescriptor.id, - userId: recipientAccount.kratosUserId, - level: recipientAccount.level, - status: recipientAccount.status, - phoneNumber: recipientUser.phone, - } - - const recipientWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: recipientWalletDescriptor.id, - journalId, - }) - if (recipientWalletTransaction instanceof Error) return recipientWalletTransaction - - // Send 'received'-side intraledger notification - NotificationsService().sendTransaction({ - recipient: recipientAsNotificationRecipient, - transaction: recipientWalletTransaction, - }) - - const senderAsNotificationRecipient = { - accountId: senderAccount.id, - walletId: senderWalletId, - userId: senderAccount.kratosUserId, - level: senderAccount.level, - status: senderAccount.status, - phoneNumber: senderUser.phone, - } + recipientAccountId: recipientAccount.id, + usdPaymentAmount: paymentFlow.usdPaymentAmount, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount: paymentFlow.btcPaymentAmount, + execute: async () => { + const journalId = await LockService().lockWalletId(senderWalletId, async (signal) => + lockedPaymentViaIntraledgerSteps({ + signal, + + paymentFlow, + senderDisplayCurrency: senderAccount.displayCurrency, + senderUsername: senderAccount.username, + recipientDisplayCurrency: recipientAccount.displayCurrency, + recipientUsername: recipientAccount.username, + + memo, + sendAll, + }), + ) + if (journalId instanceof Error) { + return reverseSettlement({ result: journalId }) + } + + const recipientAsNotificationRecipient = { + accountId: recipientAccount.id, + walletId: recipientWalletDescriptor.id, + userId: recipientAccount.kratosUserId, + level: recipientAccount.level, + status: recipientAccount.status, + phoneNumber: recipientUser.phone, + } + + const recipientWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: recipientWalletDescriptor.id, + journalId, + }) + if (recipientWalletTransaction instanceof Error) { + return recordSettlement({ + result: recipientWalletTransaction, + settlementTransactionId: journalId, + }) + } + + // Send 'received'-side intraledger notification + NotificationsService().sendTransaction({ + recipient: recipientAsNotificationRecipient, + transaction: recipientWalletTransaction, + }) - const senderWalletTransaction = await getTransactionForWalletByJournalId({ - walletId: senderWalletId, - journalId, - }) - if (senderWalletTransaction instanceof Error) return senderWalletTransaction + const senderAsNotificationRecipient = { + accountId: senderAccount.id, + walletId: senderWalletId, + userId: senderAccount.kratosUserId, + level: senderAccount.level, + status: senderAccount.status, + phoneNumber: senderUser.phone, + } + + const senderWalletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletId, + journalId, + }) + if (senderWalletTransaction instanceof Error) { + return recordSettlement({ + result: senderWalletTransaction, + settlementTransactionId: journalId, + }) + } + + NotificationsService().sendTransaction({ + recipient: senderAsNotificationRecipient, + transaction: senderWalletTransaction, + }) - NotificationsService().sendTransaction({ - recipient: senderAsNotificationRecipient, - transaction: senderWalletTransaction, + return recordSettlement({ + result: { + status: PaymentSendStatus.Success, + transaction: senderWalletTransaction, + }, + settlementTransactionId: journalId, + }) + }, }) - - return { - status: PaymentSendStatus.Success, - transaction: senderWalletTransaction, - } + return paymentSendResult } const lockedPaymentViaIntraledgerSteps = async < @@ -523,6 +543,7 @@ const executePaymentViaOnChain = async < memo, sendAll, logger, + apiKeyId, }: { builder: OPFBWithConversion | OPFBWithError senderDisplayCurrency: DisplayCurrency @@ -530,6 +551,7 @@ const executePaymentViaOnChain = async < memo: string | null sendAll: boolean logger: Logger + apiKeyId?: ApiKeyId }): Promise => { const senderWalletDescriptor = await builder.senderWalletDescriptor() if (senderWalletDescriptor instanceof Error) return senderWalletDescriptor @@ -541,38 +563,52 @@ const executePaymentViaOnChain = async < const priceRatioForLimits = await getPriceRatioForLimits(proposedAmounts) if (priceRatioForLimits instanceof Error) return priceRatioForLimits - const limitCheck = await checkWithdrawalLimits({ - amount: proposedAmounts.usd, + const paymentSendResult = await withSpendingLimits({ + settlementMethod: SettlementMethod.OnChain, accountId: senderWalletDescriptor.accountId, - priceRatio: priceRatioForLimits, - }) - if (limitCheck instanceof Error) return limitCheck - - const journalId = await LockService().lockWalletId( - senderWalletDescriptor.id, - async (signal) => - lockedPaymentViaOnChainSteps({ - signal, - - builder, - speed, - - senderDisplayCurrency, - memo, - sendAll, - - logger, - }), - ) - if (journalId instanceof Error) return journalId - - const walletTransaction = await getTransactionForWalletByJournalId({ - walletId: senderWalletDescriptor.id, - journalId, + usdPaymentAmount: proposedAmounts.usd, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount: proposedAmounts.btc, + execute: async () => { + const journalId = await LockService().lockWalletId( + senderWalletDescriptor.id, + async (signal) => + lockedPaymentViaOnChainSteps({ + signal, + + builder, + speed, + + senderDisplayCurrency, + memo, + sendAll, + + logger, + }), + ) + if (journalId instanceof Error) { + return reverseSettlement({ result: journalId }) + } + + const walletTransaction = await getTransactionForWalletByJournalId({ + walletId: senderWalletDescriptor.id, + journalId, + }) + if (walletTransaction instanceof Error) { + return recordSettlement({ + result: walletTransaction, + settlementTransactionId: journalId, + }) + } + + return recordSettlement({ + result: { status: PaymentSendStatus.Success, transaction: walletTransaction }, + settlementTransactionId: journalId, + }) + }, }) - if (walletTransaction instanceof Error) return walletTransaction - - return { status: PaymentSendStatus.Success, transaction: walletTransaction } + return paymentSendResult } const lockedPaymentViaOnChainSteps = async < diff --git a/core/api/src/app/payments/spending-limits.ts b/core/api/src/app/payments/spending-limits.ts new file mode 100644 index 0000000000..3d08c97286 --- /dev/null +++ b/core/api/src/app/payments/spending-limits.ts @@ -0,0 +1,128 @@ +import { + ApiKeySpendingSettlementType, + lockApiKeySpending, + reverseApiKeySpendingSettlement, + recordApiKeySpendingSettlement, + settleApiKeySpending, +} from "./api-key-spending" + +import { + checkIntraledgerLimits, + checkTradeIntraAccountLimits, + checkWithdrawalLimits, +} from "@/app/accounts" + +import { ErrorLevel } from "@/domain/shared" +import { SettlementMethod } from "@/domain/wallets" +import { recordExceptionInCurrentSpan } from "@/services/tracing" + +export const recordSettlement = ({ + result, + settlementTransactionId, +}: { + result: PaymentSendResult | ApplicationError + settlementTransactionId: LedgerJournalId +}): SpendingLimitsExecutionResult => ({ + apiKeySettlement: ApiKeySpendingSettlementType.Record, + settlementTransactionId, + result, +}) + +export const reverseSettlement = ({ + result, +}: { + result: PaymentSendResult | ApplicationError +}): SpendingLimitsExecutionResult => ({ + apiKeySettlement: ApiKeySpendingSettlementType.Reverse, + result, +}) + +const settlementFor = ( + executionResult: SpendingLimitsExecutionResult, +): ApiKeySpendingSettlement => { + const { apiKeySettlement } = executionResult + + if ( + apiKeySettlement === ApiKeySpendingSettlementType.Record && + !executionResult.settlementTransactionId + ) { + recordExceptionInCurrentSpan({ + error: new Error( + "Invalid spending settlement result: record settlement without transaction id", + ), + level: ErrorLevel.Critical, + }) + return reverseApiKeySpendingSettlement() + } + + switch (apiKeySettlement) { + case ApiKeySpendingSettlementType.Record: + return recordApiKeySpendingSettlement(executionResult.settlementTransactionId) + + case ApiKeySpendingSettlementType.Reverse: + return reverseApiKeySpendingSettlement() + + default: { + const exhaustiveCheck: never = apiKeySettlement + return exhaustiveCheck + } + } +} + +const getLimitCheck = ({ + settlementMethod, + accountId, + recipientAccountId, +}: { + settlementMethod: SettlementMethod + accountId: AccountId + recipientAccountId?: AccountId +}) => { + if (settlementMethod !== SettlementMethod.IntraLedger) return checkWithdrawalLimits + if (accountId === recipientAccountId) return checkTradeIntraAccountLimits + return checkIntraledgerLimits +} + +export const withSpendingLimits = async ({ + settlementMethod, + accountId, + recipientAccountId, + usdPaymentAmount, + priceRatioForLimits, + apiKeyId, + btcPaymentAmount, + execute, +}: { + settlementMethod: SettlementMethod + accountId: AccountId + recipientAccountId?: AccountId + usdPaymentAmount: UsdPaymentAmount + priceRatioForLimits: WalletPriceRatio + apiKeyId?: ApiKeyId + btcPaymentAmount: BtcPaymentAmount + execute: () => Promise +}): Promise => { + const checkLimit = getLimitCheck({ settlementMethod, accountId, recipientAccountId }) + + const limitCheck = await checkLimit({ + amount: usdPaymentAmount, + accountId, + priceRatio: priceRatioForLimits, + }) + if (limitCheck instanceof Error) return limitCheck + + const lock = await lockApiKeySpending({ apiKeyId, amount: btcPaymentAmount }) + if (lock instanceof Error) return lock + + const executionResult = await execute() + + const settleResult = await settleApiKeySpending({ + lock, + settlement: settlementFor(executionResult), + }) + if (settleResult instanceof Error) { + recordExceptionInCurrentSpan({ error: settleResult }) + } + + return executionResult.result +} diff --git a/core/api/src/app/payments/update-pending-payments.ts b/core/api/src/app/payments/update-pending-payments.ts index e7c0e9b74c..9ae2ebe1dd 100644 --- a/core/api/src/app/payments/update-pending-payments.ts +++ b/core/api/src/app/payments/update-pending-payments.ts @@ -24,6 +24,7 @@ import { import { MissingPropsInTransactionForPaymentFlowError } from "@/domain/payments" import { setErrorCritical, WalletCurrency } from "@/domain/shared" +import { ApiKeysService } from "@/services/api-keys" import { LedgerService, getNonEndUserWalletIds } from "@/services/ledger" import * as LedgerFacade from "@/services/ledger/facade" import { LndService } from "@/services/lnd" @@ -348,6 +349,7 @@ const lockedPendingPaymentSteps = async ({ { success: false, id: paymentHash, payment: pendingPayment }, "payment has failed. reverting transaction", ) + const apiKeys = ApiKeysService() if (paymentFlow.senderWalletCurrency === WalletCurrency.Btc) { const voided = await ledgerService.revertLightningPayment({ journalId, @@ -358,7 +360,18 @@ const lockedPendingPaymentSteps = async ({ paymentLogger.fatal({ success: false, result: lnPaymentLookup }, error) return setErrorCritical(voided) } - + const reverseResult = await apiKeys.reverseSpending({ + transactionId: journalId, + }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ + error: reverseResult, + attributes: { + "apiKeys.reverseSpending.failed": true, + "journalId": journalId, + }, + }) + } return finalizePaymentUpdate({ result: voided, walletIds, @@ -378,7 +391,18 @@ const lockedPendingPaymentSteps = async ({ paymentLogger.fatal({ success: false, result: lnPaymentLookup }, error) return setErrorCritical(reimbursed) } - + const reverseResult = await apiKeys.reverseSpending({ + transactionId: journalId, + }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ + error: reverseResult, + attributes: { + "apiKeys.reverseSpending.failed": true, + "journalId": journalId, + }, + }) + } return finalizePaymentUpdate({ result: reimbursed, walletIds, diff --git a/core/api/src/app/wallets/index.types.d.ts b/core/api/src/app/wallets/index.types.d.ts index 147498f45f..2dee0f6d3f 100644 --- a/core/api/src/app/wallets/index.types.d.ts +++ b/core/api/src/app/wallets/index.types.d.ts @@ -98,6 +98,7 @@ type PaymentSendArgs = { senderWalletId: WalletId senderAccount: Account memo: string | null + apiKeyId?: ApiKeyId } type PayInvoiceByWalletIdArgs = PaymentSendArgs & { @@ -127,6 +128,7 @@ type PayAllOnChainByWalletIdArgs = { address: string speed: PayoutSpeed memo: string | null + apiKeyId?: ApiKeyId } type PayOnChainByWalletIdWithoutCurrencyArgs = { @@ -136,6 +138,7 @@ type PayOnChainByWalletIdWithoutCurrencyArgs = { address: string speed: PayoutSpeed memo: string | null + apiKeyId?: ApiKeyId } type PayOnChainByWalletIdArgs = PayOnChainByWalletIdWithoutCurrencyArgs & { @@ -148,6 +151,7 @@ type LnAddressPaymentSendArgs = { senderAccount: Account lnAddress: string amount: number + apiKeyId?: ApiKeyId } type LnurlPaymentSendArgs = { @@ -155,6 +159,7 @@ type LnurlPaymentSendArgs = { senderAccount: Account lnurl: string amount: number + apiKeyId?: ApiKeyId } type GetDepositFeeConfigurationResult = { diff --git a/core/api/src/config/env.ts b/core/api/src/config/env.ts index bd6c2c4c39..881915c9d8 100644 --- a/core/api/src/config/env.ts +++ b/core/api/src/config/env.ts @@ -64,6 +64,9 @@ export const env = createEnv({ .pipe(z.coerce.number()) .default(6685), + API_KEYS_HOST: z.string().min(1).default("localhost"), + API_KEYS_PORT: z.number().min(1).or(z.string()).pipe(z.coerce.number()).default(6686), + GEETEST_ID: z.string().min(1).optional(), GEETEST_KEY: z.string().min(1).optional(), @@ -197,6 +200,9 @@ export const env = createEnv({ NOTIFICATIONS_HOST: process.env.NOTIFICATIONS_HOST, NOTIFICATIONS_PORT: process.env.NOTIFICATIONS_PORT, + API_KEYS_HOST: process.env.API_KEYS_HOST, + API_KEYS_PORT: process.env.API_KEYS_PORT, + GEETEST_ID: process.env.GEETEST_ID, GEETEST_KEY: process.env.GEETEST_KEY, diff --git a/core/api/src/config/index.ts b/core/api/src/config/index.ts index 3548c2cf94..2be58a6437 100644 --- a/core/api/src/config/index.ts +++ b/core/api/src/config/index.ts @@ -167,6 +167,8 @@ export const KRATOS_MASTER_USER_PASSWORD = env.KRATOS_MASTER_USER_PASSWORD export const BRIA_HOST = env.BRIA_HOST export const BRIA_PORT = env.BRIA_PORT export const BRIA_API_KEY = env.BRIA_API_KEY +export const API_KEYS_HOST = env.API_KEYS_HOST +export const API_KEYS_PORT = env.API_KEYS_PORT export const NOTIFICATIONS_HOST = env.NOTIFICATIONS_HOST export const NOTIFICATIONS_PORT = env.NOTIFICATIONS_PORT export const GEETEST_ID = env.GEETEST_ID diff --git a/core/api/src/domain/api-keys/errors.ts b/core/api/src/domain/api-keys/errors.ts new file mode 100644 index 0000000000..c5135b7c57 --- /dev/null +++ b/core/api/src/domain/api-keys/errors.ts @@ -0,0 +1,21 @@ +import { DomainError, ErrorLevel } from "@/domain/shared" + +export class ApiKeysServiceError extends DomainError {} + +export class ApiKeyLimitExceededError extends ApiKeysServiceError { + level = ErrorLevel.Info +} + +export class ApiKeyInvalidLimitError extends ApiKeysServiceError {} + +export class ApiKeySpendingRecordError extends ApiKeysServiceError { + level = ErrorLevel.Critical +} + +export class InvalidApiKeyIdError extends ApiKeysServiceError { + level = ErrorLevel.Warn +} + +export class UnknownApiKeysServiceError extends ApiKeysServiceError { + level = ErrorLevel.Critical +} diff --git a/core/api/src/domain/api-keys/index.ts b/core/api/src/domain/api-keys/index.ts new file mode 100644 index 0000000000..a079f46484 --- /dev/null +++ b/core/api/src/domain/api-keys/index.ts @@ -0,0 +1 @@ +export * from "./errors" diff --git a/core/api/src/domain/api-keys/index.types.d.ts b/core/api/src/domain/api-keys/index.types.d.ts new file mode 100644 index 0000000000..48961c1ecc --- /dev/null +++ b/core/api/src/domain/api-keys/index.types.d.ts @@ -0,0 +1,23 @@ +type ApiKeyId = string & { readonly brand: unique symbol } + +type EphemeralId = string & { readonly brand: unique symbol } + +type ApiKeysServiceError = import("./errors").ApiKeysServiceError + +interface IApiKeysService { + checkAndLockSpending(args: { + apiKeyId: ApiKeyId + amount: BtcPaymentAmount + }): Promise + + recordSpending(args: { + apiKeyId: ApiKeyId + amount: BtcPaymentAmount + transactionId: LedgerJournalId + ephemeralId: EphemeralId + }): Promise + + reverseSpending(args: { + transactionId: LedgerJournalId | EphemeralId + }): Promise +} diff --git a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts index c61a0e8d0f..ddcf17f9b3 100644 --- a/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts +++ b/core/api/src/domain/bitcoin/lightning/payments/index.types.d.ts @@ -5,11 +5,11 @@ type LnPaymentPartial = { } // Makes all properties non-readonly except the properties passed in as K -type Writeable = Pick & { +type Writable = Pick & { -readonly [P in keyof T as Exclude]: T[P] } -type PersistedLnPaymentLookup = Writeable & { +type PersistedLnPaymentLookup = Writable & { readonly sentFromPubkey: Pubkey isCompleteRecord: boolean } diff --git a/core/api/src/domain/shared/errors.ts b/core/api/src/domain/shared/errors.ts index 6290707aeb..4dc5a55842 100644 --- a/core/api/src/domain/shared/errors.ts +++ b/core/api/src/domain/shared/errors.ts @@ -31,6 +31,7 @@ export class BigIntFloatConversionError extends BigIntConversionError {} export class UnknownBigIntConversionError extends BigIntConversionError { level = ErrorLevel.Critical } +export class BigIntToNumberConversionError extends BigIntConversionError {} export class BtcAmountTooLargeError extends ValidationError {} export class UsdAmountTooLargeError extends ValidationError {} diff --git a/core/api/src/domain/shared/safe.ts b/core/api/src/domain/shared/safe.ts index 8d7c73445e..a549825331 100644 --- a/core/api/src/domain/shared/safe.ts +++ b/core/api/src/domain/shared/safe.ts @@ -1,4 +1,8 @@ -import { BigIntFloatConversionError, UnknownBigIntConversionError } from "./errors" +import { + BigIntFloatConversionError, + BigIntToNumberConversionError, + UnknownBigIntConversionError, +} from "./errors" export const safeBigInt = (num: number | string): bigint | BigIntConversionError => { try { @@ -14,3 +18,19 @@ export const safeBigInt = (num: number | string): bigint | BigIntConversionError export const roundToBigInt = (num: number): bigint => { return BigInt(Math.round(num)) } + +export const safeIntFromBigInt = ( + value: bigint, +): number | BigIntToNumberConversionError => { + if (value > BigInt(Number.MAX_SAFE_INTEGER)) { + return new BigIntToNumberConversionError( + `BigInt value ${value} exceeds Number.MAX_SAFE_INTEGER and cannot be safely converted to number`, + ) + } + if (value < BigInt(Number.MIN_SAFE_INTEGER)) { + return new BigIntToNumberConversionError( + `BigInt value ${value} is below Number.MIN_SAFE_INTEGER and cannot be safely converted to number`, + ) + } + return Number(value) +} diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index 025a294941..a698df93e0 100644 --- a/core/api/src/graphql/error-map.ts +++ b/core/api/src/graphql/error-map.ts @@ -63,6 +63,22 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { message = error.message return new TransactionRestrictedError({ message, logger: baseLogger }) + case "ApiKeyLimitExceededError": + message = error.message + return new TransactionRestrictedError({ message, logger: baseLogger }) + + case "ApiKeyInvalidLimitError": + message = error.message || "Failed to check API key spending limit" + return new ValidationInternalError({ message, logger: baseLogger }) + + case "ApiKeySpendingRecordError": + message = error.message || "Failed to record API key spending" + return new ValidationInternalError({ message, logger: baseLogger }) + + case "InvalidApiKeyIdError": + message = error.message || "Invalid API key ID" + return new ValidationInternalError({ message, logger: baseLogger }) + case "AlreadyPaidError": message = "Invoice is already paid" return new LightningPaymentError({ message, logger: baseLogger }) @@ -195,6 +211,10 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { message = "A valid usd amount is required" return new ValidationInternalError({ message, logger: baseLogger }) + case "BigIntToNumberConversionError": + message = "Sats amount passed is too large to be safely converted" + return new ValidationInternalError({ message, logger: baseLogger }) + case "BtcAmountTooLargeError": message = "Sats amount passed is too large" return new ValidationInternalError({ message, logger: baseLogger }) @@ -615,6 +635,7 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "PayoutQueueNotFoundError": message = "Invalid or inactive speed" return new ValidationInternalError({ message, logger: baseLogger }) + // ---------- // Unhandled below here // ---------- @@ -825,6 +846,7 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "InvalidErrorCodeForPhoneMetadataError": case "InvalidCountryCodeForPhoneMetadataError": case "MultipleWalletsFoundForAccountIdAndCurrency": + case "ApiKeysServiceError": message = `Unexpected error occurred, please try again or contact support if it persists (code: ${ error.name }${error.message ? ": " + error.message : ""})` @@ -886,6 +908,7 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => { case "UnknownBigIntConversionError": case "UnknownDomainError": case "UnknownBriaEventError": + case "UnknownApiKeysServiceError": case "CouldNotFindAccountError": case "OathkeeperError": case "OathkeeperUnauthorizedServiceError": diff --git a/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts b/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts index 0744d46af0..191d418a99 100644 --- a/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/intraledger-payment-send.ts @@ -30,7 +30,7 @@ const IntraLedgerPaymentSendMutation = GT.Field( args: { input: { type: GT.NonNull(IntraLedgerPaymentSendInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, recipientWalletId, amount, memo } = args.input for (const input of [walletId, recipientWalletId, amount, memo]) { if (input instanceof Error) { @@ -54,6 +54,7 @@ const IntraLedgerPaymentSendMutation = GT.Field( amount, senderWalletId: walletId, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { return { status: "failed", errors: [mapAndParseErrorForGqlResponse(result)] } diff --git a/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts b/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts index 0aa7dd3a3d..14457d251d 100644 --- a/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/intraledger-usd-payment-send.ts @@ -30,7 +30,7 @@ const IntraLedgerUsdPaymentSendMutation = GT.Field { + resolve: async (_, args, { domainAccount, apiKeyId }: GraphQLPublicContextAuth) => { const { walletId, recipientWalletId, amount, memo } = args.input for (const input of [walletId, recipientWalletId, amount, memo]) { if (input instanceof Error) { @@ -54,6 +54,7 @@ const IntraLedgerUsdPaymentSendMutation = GT.Field { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, amount, lnAddress } = args.input if (lnAddress instanceof Error) { return { errors: [{ message: lnAddress.message }] } @@ -62,6 +62,7 @@ const LnAddressPaymentSendMutation = GT.Field< amount, senderWalletId: walletId, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/ln-invoice-payment-send.ts b/core/api/src/graphql/public/root/mutation/ln-invoice-payment-send.ts index 033c539a32..bc798d0782 100644 --- a/core/api/src/graphql/public/root/mutation/ln-invoice-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/ln-invoice-payment-send.ts @@ -49,7 +49,7 @@ const LnInvoicePaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(LnInvoicePaymentInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, paymentRequest, memo } = args.input if (walletId instanceof InputValidationError) { return { errors: [{ message: walletId.message }] } @@ -66,6 +66,7 @@ const LnInvoicePaymentSendMutation = GT.Field< uncheckedPaymentRequest: paymentRequest, memo: memo ?? null, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/ln-noamount-invoice-payment-send.ts b/core/api/src/graphql/public/root/mutation/ln-noamount-invoice-payment-send.ts index c3f6db9aca..7f8d0698b7 100644 --- a/core/api/src/graphql/public/root/mutation/ln-noamount-invoice-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/ln-noamount-invoice-payment-send.ts @@ -55,7 +55,7 @@ const LnNoAmountInvoicePaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(LnNoAmountInvoicePaymentInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, paymentRequest, amount, memo } = args.input if (walletId instanceof InputValidationError) { @@ -77,6 +77,7 @@ const LnNoAmountInvoicePaymentSendMutation = GT.Field< memo: memo ?? null, amount, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts b/core/api/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts index c025c6d206..ac9811fa6e 100644 --- a/core/api/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/ln-noamount-usd-invoice-payment-send.ts @@ -52,7 +52,7 @@ const LnNoAmountUsdInvoicePaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(LnNoAmountUsdInvoicePaymentInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, paymentRequest, amount, memo } = args.input if (walletId instanceof InputValidationError) { @@ -74,6 +74,7 @@ const LnNoAmountUsdInvoicePaymentSendMutation = GT.Field< memo: memo ?? null, amount, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/lnurl-payment-send.ts b/core/api/src/graphql/public/root/mutation/lnurl-payment-send.ts index 18dcfc239f..73aea1d835 100644 --- a/core/api/src/graphql/public/root/mutation/lnurl-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/lnurl-payment-send.ts @@ -43,7 +43,7 @@ const LnurlPaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(LnurlPaymentSendInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, amount, lnurl } = args.input if (lnurl instanceof Error) { return { errors: [{ message: lnurl.message }] } @@ -62,6 +62,7 @@ const LnurlPaymentSendMutation = GT.Field< amount, senderWalletId: walletId, senderAccount: domainAccount, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/onchain-payment-send-all.ts b/core/api/src/graphql/public/root/mutation/onchain-payment-send-all.ts index 7382c84dc7..d392b0b4ff 100644 --- a/core/api/src/graphql/public/root/mutation/onchain-payment-send-all.ts +++ b/core/api/src/graphql/public/root/mutation/onchain-payment-send-all.ts @@ -42,7 +42,7 @@ const OnChainPaymentSendAllMutation = GT.Field< args: { input: { type: GT.NonNull(OnChainPaymentSendAllInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, address, memo, speed } = args.input if (walletId instanceof Error) { @@ -67,6 +67,7 @@ const OnChainPaymentSendAllMutation = GT.Field< address, speed, memo, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/onchain-payment-send.ts b/core/api/src/graphql/public/root/mutation/onchain-payment-send.ts index 79c0476940..3efd51ed7e 100644 --- a/core/api/src/graphql/public/root/mutation/onchain-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/onchain-payment-send.ts @@ -45,7 +45,7 @@ const OnChainPaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(OnChainPaymentSendInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, address, amount, memo, speed } = args.input if (walletId instanceof Error) { @@ -75,6 +75,7 @@ const OnChainPaymentSendMutation = GT.Field< address, speed, memo, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send-as-sats.ts b/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send-as-sats.ts index 3e16c5ae50..7b4f0e69ba 100644 --- a/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send-as-sats.ts +++ b/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send-as-sats.ts @@ -45,7 +45,7 @@ const OnChainUsdPaymentSendAsBtcDenominatedMutation = GT.Field< args: { input: { type: GT.NonNull(OnChainUsdPaymentSendAsBtcDenominatedInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, address, amount, memo, speed } = args.input if (walletId instanceof Error) { @@ -71,6 +71,7 @@ const OnChainUsdPaymentSendAsBtcDenominatedMutation = GT.Field< address, speed, memo, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send.ts b/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send.ts index 4f9b1a63e0..44e555d6b5 100644 --- a/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send.ts +++ b/core/api/src/graphql/public/root/mutation/onchain-usd-payment-send.ts @@ -45,7 +45,7 @@ const OnChainUsdPaymentSendMutation = GT.Field< args: { input: { type: GT.NonNull(OnChainUsdPaymentSendInput) }, }, - resolve: async (_, args, { domainAccount }) => { + resolve: async (_, args, { domainAccount, apiKeyId }) => { const { walletId, address, amount, memo, speed } = args.input if (walletId instanceof Error) { @@ -71,6 +71,7 @@ const OnChainUsdPaymentSendMutation = GT.Field< address, speed, memo, + apiKeyId, }) if (result instanceof Error) { diff --git a/core/api/src/servers/event-handlers/bria.ts b/core/api/src/servers/event-handlers/bria.ts index 457120bdfb..17b01bf5da 100644 --- a/core/api/src/servers/event-handlers/bria.ts +++ b/core/api/src/servers/event-handlers/bria.ts @@ -11,6 +11,7 @@ import { recordExceptionInCurrentSpan, addAttributesToCurrentSpan, } from "@/services/tracing" +import { ApiKeysService } from "@/services/api-keys" import { EventAugmentationMissingError, UnknownPayloadTypeReceivedError, @@ -24,6 +25,8 @@ import { NoTransactionToSettleError } from "@/services/ledger/domain/errors" const assertUnreachable = (payloadType: never): Error => new UnknownPayloadTypeReceivedError(payloadType) +const apiKeys = ApiKeysService() + const isBriaPayoutEvent = (payload: BriaPayload): payload is BriaPayoutPayload => { return (payload as BriaPayoutPayload).id !== undefined } @@ -180,15 +183,32 @@ export const payoutCancelledEventHandler = async ({ event: PayoutCancelled payoutInfo: PayoutAugmentation }): Promise => { + const journalId = payoutInfo.externalId as LedgerJournalId const res = await LedgerFacade.recordOnChainSendRevert({ - journalId: payoutInfo.externalId as LedgerJournalId, + journalId, payoutId: event.id, }) if (res instanceof NoTransactionToUpdateError) { return true } + if (res instanceof Error) { + return res + } - return res + const reverseResult = await apiKeys.reverseSpending({ + transactionId: journalId, + }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ + error: reverseResult, + attributes: { + "apiKeys.reverseSpending.failed": true, + "journalId": payoutInfo.externalId, + }, + }) + } + + return true } export const payoutBroadcastEventHandler = async ({ diff --git a/core/api/src/servers/index.files.d.ts b/core/api/src/servers/index.files.d.ts index add09bc539..0c70ee544f 100644 --- a/core/api/src/servers/index.files.d.ts +++ b/core/api/src/servers/index.files.d.ts @@ -18,6 +18,7 @@ type GraphQLPublicContextAuth = GraphQLPublicContext & { domainAccount: Account scope: ScopesOauth2[] | undefined appId: string | undefined + apiKeyId?: ApiKeyId } type GraphQLAdminContext = { diff --git a/core/api/src/servers/middlewares/session.ts b/core/api/src/servers/middlewares/session.ts index 8cda27eb1f..7320341a2e 100644 --- a/core/api/src/servers/middlewares/session.ts +++ b/core/api/src/servers/middlewares/session.ts @@ -30,6 +30,10 @@ export const sessionPublicContext = async ({ ) const sub = tokenPayload?.sub const appId = tokenPayload?.client_id + let apiKeyId: ApiKeyId | undefined + if (typeof tokenPayload?.api_key_id === "string") { + apiKeyId = tokenPayload.api_key_id as ApiKeyId + } // note: value should match (ie: "anon") if not an accountId // settings from dev/ory/oathkeeper.yml/authenticator/anonymous/config/subjet @@ -87,6 +91,7 @@ export const sessionPublicContext = async ({ sessionId, scope, appId, + apiKeyId, } } diff --git a/core/api/src/services/api-keys/errors.ts b/core/api/src/services/api-keys/errors.ts new file mode 100644 index 0000000000..f453907bf5 --- /dev/null +++ b/core/api/src/services/api-keys/errors.ts @@ -0,0 +1,45 @@ +import { + ApiKeyInvalidLimitError, + ApiKeyLimitExceededError, + ApiKeySpendingRecordError, + InvalidApiKeyIdError, + UnknownApiKeysServiceError, +} from "@/domain/api-keys" +import { parseErrorMessageFromUnknown } from "@/domain/shared" + +export const handleCommonApiKeysErrors = (err: Error | string | unknown) => { + const errMsg = parseErrorMessageFromUnknown(err) + + const match = (knownErrDetail: RegExp): boolean => knownErrDetail.test(errMsg) + + switch (true) { + case match(KnownApiKeysErrorMessages.LimitExceeded): + return new ApiKeyLimitExceededError(errMsg) + + case match(KnownApiKeysErrorMessages.InvalidApiKeyId): + return new InvalidApiKeyIdError(errMsg) + + case match(KnownApiKeysErrorMessages.InvalidAmountError): + case match(KnownApiKeysErrorMessages.AmountMismatchError): + case match(KnownApiKeysErrorMessages.MissingTransactionIdError): + case match(KnownApiKeysErrorMessages.EphemeralNotFoundError): + return new ApiKeySpendingRecordError(errMsg) + + case match(KnownApiKeysErrorMessages.InvalidLimitError): + return new ApiKeyInvalidLimitError(errMsg) + + default: + return new UnknownApiKeysServiceError(errMsg) + } +} + +export const KnownApiKeysErrorMessages = { + LimitExceeded: /spending limit exceeded/, + InvalidApiKeyId: /Invalid API key ID/, + InvalidAmountError: + /Negative amount not allowed|Amount must be positive|Invalid limit amount \(must be positive\)/, + AmountMismatchError: /Spending amount mismatch for transaction reference/, + MissingTransactionIdError: /Missing transaction id for ephemeral finalization/, + EphemeralNotFoundError: /Ephemeral reservation not found:/, + InvalidLimitError: /Invalid limit value/, +} as const diff --git a/core/api/src/services/api-keys/grpc-client.ts b/core/api/src/services/api-keys/grpc-client.ts new file mode 100644 index 0000000000..1723645e34 --- /dev/null +++ b/core/api/src/services/api-keys/grpc-client.ts @@ -0,0 +1,43 @@ +import { promisify } from "util" + +import { credentials, Metadata } from "@grpc/grpc-js" + +import { ApiKeysServiceClient } from "./proto/api_keys_grpc_pb" + +import { + CheckAndLockSpendingRequest, + CheckAndLockSpendingResponse, + RecordSpendingRequest, + RecordSpendingResponse, + ReverseSpendingRequest, + ReverseSpendingResponse, +} from "./proto/api_keys_pb" + +import { API_KEYS_HOST, API_KEYS_PORT } from "@/config" + +const apiKeysEndpoint = `${API_KEYS_HOST}:${API_KEYS_PORT}` + +const apiKeysClient = new ApiKeysServiceClient( + apiKeysEndpoint, + credentials.createInsecure(), +) + +export const apiKeysMetadata = new Metadata() + +export const checkAndLockSpending = promisify< + CheckAndLockSpendingRequest, + Metadata, + CheckAndLockSpendingResponse +>(apiKeysClient.checkAndLockSpending.bind(apiKeysClient)) + +export const recordSpending = promisify< + RecordSpendingRequest, + Metadata, + RecordSpendingResponse +>(apiKeysClient.recordSpending.bind(apiKeysClient)) + +export const reverseSpending = promisify< + ReverseSpendingRequest, + Metadata, + ReverseSpendingResponse +>(apiKeysClient.reverseSpending.bind(apiKeysClient)) diff --git a/core/api/src/services/api-keys/index.ts b/core/api/src/services/api-keys/index.ts new file mode 100644 index 0000000000..6244bada3d --- /dev/null +++ b/core/api/src/services/api-keys/index.ts @@ -0,0 +1,100 @@ +import { + CheckAndLockSpendingRequest, + RecordSpendingRequest, + ReverseSpendingRequest, +} from "./proto/api_keys_pb" + +import * as apiKeysGrpc from "./grpc-client" + +import { handleCommonApiKeysErrors } from "./errors" + +import { safeIntFromBigInt } from "@/domain/shared" +import { baseLogger } from "@/services/logger" +import { wrapAsyncFunctionsToRunInSpan } from "@/services/tracing" + +export const ApiKeysService = (): IApiKeysService => { + const checkAndLockSpending = async ({ + apiKeyId, + amount, + }: { + apiKeyId: ApiKeyId + amount: BtcPaymentAmount + }): Promise => { + try { + const amountSats = safeIntFromBigInt(amount.amount) + if (amountSats instanceof Error) return amountSats + const request = new CheckAndLockSpendingRequest() + request.setApiKeyId(apiKeyId) + request.setAmountSats(amountSats) + + const response = await apiKeysGrpc.checkAndLockSpending( + request, + apiKeysGrpc.apiKeysMetadata, + ) + + return response.getEphemeralId() as EphemeralId + } catch (err) { + baseLogger.error({ err, apiKeyId, amount }, "Failed to check and lock spending") + return handleCommonApiKeysErrors(err) + } + } + + const recordSpending = async ({ + apiKeyId, + amount, + transactionId, + ephemeralId, + }: { + apiKeyId: ApiKeyId + amount: BtcPaymentAmount + transactionId: LedgerJournalId + ephemeralId: EphemeralId + }): Promise => { + try { + const amountSats = safeIntFromBigInt(amount.amount) + if (amountSats instanceof Error) return amountSats + const request = new RecordSpendingRequest() + request.setApiKeyId(apiKeyId) + request.setAmountSats(amountSats) + request.setTransactionId(transactionId) + request.setEphemeralId(ephemeralId) + + await apiKeysGrpc.recordSpending(request, apiKeysGrpc.apiKeysMetadata) + + return true + } catch (err) { + baseLogger.error( + { err, apiKeyId, amount, transactionId, ephemeralId }, + "Failed to record API key spending", + ) + return handleCommonApiKeysErrors(err) + } + } + + const reverseSpending = async ({ + transactionId, + }: { + transactionId: LedgerJournalId | EphemeralId + }): Promise => { + try { + const request = new ReverseSpendingRequest() + request.setTransactionId(transactionId) + + await apiKeysGrpc.reverseSpending(request, apiKeysGrpc.apiKeysMetadata) + + return true + } catch (err) { + baseLogger.error({ err, transactionId }, "Failed to reverse API key spending") + return handleCommonApiKeysErrors(err) + } + } + + return wrapAsyncFunctionsToRunInSpan({ + namespace: "services.api-keys", + fns: { + checkAndLockSpending, + recordSpending, + reverseSpending, + }, + }) +} diff --git a/core/api/src/services/api-keys/proto/api_keys.proto b/core/api/src/services/api-keys/proto/api_keys.proto new file mode 100644 index 0000000000..ffd4bd88a6 --- /dev/null +++ b/core/api/src/services/api-keys/proto/api_keys.proto @@ -0,0 +1,77 @@ +syntax = "proto3"; + +package services.api_keys.v1; + +service ApiKeysService { + rpc CheckSpendingLimit (CheckSpendingLimitRequest) returns (CheckSpendingLimitResponse) {} + rpc CheckAndLockSpending (CheckAndLockSpendingRequest) returns (CheckAndLockSpendingResponse) {} + rpc GetSpendingSummary (GetSpendingSummaryRequest) returns (GetSpendingSummaryResponse) {} + rpc RecordSpending (RecordSpendingRequest) returns (RecordSpendingResponse) {} + rpc ReverseSpending (ReverseSpendingRequest) returns (ReverseSpendingResponse) {} +} + +message CheckSpendingLimitRequest { + string api_key_id = 1; + int64 amount_sats = 2; +} + +message CheckSpendingLimitResponse { + bool allowed = 1; + optional int64 daily_limit_sats = 2; + optional int64 weekly_limit_sats = 3; + optional int64 monthly_limit_sats = 4; + optional int64 annual_limit_sats = 5; + int64 daily_spent_sats = 6; + int64 weekly_spent_sats = 7; + int64 monthly_spent_sats = 8; + int64 annual_spent_sats = 9; + optional int64 remaining_daily_sats = 10; + optional int64 remaining_weekly_sats = 11; + optional int64 remaining_monthly_sats = 12; + optional int64 remaining_annual_sats = 13; +} + +message CheckAndLockSpendingRequest { + string api_key_id = 1; + int64 amount_sats = 2; +} + +message CheckAndLockSpendingResponse { + string ephemeral_id = 1; +} + +message GetSpendingSummaryRequest { + string api_key_id = 1; +} + +message GetSpendingSummaryResponse { + optional int64 daily_limit_sats = 1; + optional int64 weekly_limit_sats = 2; + optional int64 monthly_limit_sats = 3; + optional int64 annual_limit_sats = 4; + int64 daily_spent_sats = 5; + int64 weekly_spent_sats = 6; + int64 monthly_spent_sats = 7; + int64 annual_spent_sats = 8; + optional int64 remaining_daily_sats = 9; + optional int64 remaining_weekly_sats = 10; + optional int64 remaining_monthly_sats = 11; + optional int64 remaining_annual_sats = 12; +} + +message RecordSpendingRequest { + string api_key_id = 1; + int64 amount_sats = 2; + optional string transaction_id = 3; + optional string ephemeral_id = 4; +} + +message RecordSpendingResponse { +} + +message ReverseSpendingRequest { + string transaction_id = 1; +} + +message ReverseSpendingResponse { +} diff --git a/core/api/src/services/api-keys/proto/api_keys_grpc_pb.d.ts b/core/api/src/services/api-keys/proto/api_keys_grpc_pb.d.ts new file mode 100644 index 0000000000..10e4e9bba2 --- /dev/null +++ b/core/api/src/services/api-keys/proto/api_keys_grpc_pb.d.ts @@ -0,0 +1,109 @@ +// package: services.api_keys.v1 +// file: api_keys.proto + +/* tslint:disable */ +/* eslint-disable */ + +import * as grpc from "@grpc/grpc-js"; +import * as api_keys_pb from "./api_keys_pb"; + +interface IApiKeysServiceService extends grpc.ServiceDefinition { + checkSpendingLimit: IApiKeysServiceService_ICheckSpendingLimit; + checkAndLockSpending: IApiKeysServiceService_ICheckAndLockSpending; + getSpendingSummary: IApiKeysServiceService_IGetSpendingSummary; + recordSpending: IApiKeysServiceService_IRecordSpending; + reverseSpending: IApiKeysServiceService_IReverseSpending; +} + +interface IApiKeysServiceService_ICheckSpendingLimit extends grpc.MethodDefinition { + path: "/services.api_keys.v1.ApiKeysService/CheckSpendingLimit"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface IApiKeysServiceService_ICheckAndLockSpending extends grpc.MethodDefinition { + path: "/services.api_keys.v1.ApiKeysService/CheckAndLockSpending"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface IApiKeysServiceService_IGetSpendingSummary extends grpc.MethodDefinition { + path: "/services.api_keys.v1.ApiKeysService/GetSpendingSummary"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface IApiKeysServiceService_IRecordSpending extends grpc.MethodDefinition { + path: "/services.api_keys.v1.ApiKeysService/RecordSpending"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface IApiKeysServiceService_IReverseSpending extends grpc.MethodDefinition { + path: "/services.api_keys.v1.ApiKeysService/ReverseSpending"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} + +export const ApiKeysServiceService: IApiKeysServiceService; + +export interface IApiKeysServiceServer extends grpc.UntypedServiceImplementation { + checkSpendingLimit: grpc.handleUnaryCall; + checkAndLockSpending: grpc.handleUnaryCall; + getSpendingSummary: grpc.handleUnaryCall; + recordSpending: grpc.handleUnaryCall; + reverseSpending: grpc.handleUnaryCall; +} + +export interface IApiKeysServiceClient { + checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + recordSpending(request: api_keys_pb.RecordSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + recordSpending(request: api_keys_pb.RecordSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + recordSpending(request: api_keys_pb.RecordSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + reverseSpending(request: api_keys_pb.ReverseSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; + reverseSpending(request: api_keys_pb.ReverseSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; + reverseSpending(request: api_keys_pb.ReverseSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; +} + +export class ApiKeysServiceClient extends grpc.Client implements IApiKeysServiceClient { + constructor(address: string, credentials: grpc.ChannelCredentials, options?: Partial); + public checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + public checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + public checkSpendingLimit(request: api_keys_pb.CheckSpendingLimitRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckSpendingLimitResponse) => void): grpc.ClientUnaryCall; + public checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + public checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + public checkAndLockSpending(request: api_keys_pb.CheckAndLockSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.CheckAndLockSpendingResponse) => void): grpc.ClientUnaryCall; + public getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + public getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + public getSpendingSummary(request: api_keys_pb.GetSpendingSummaryRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.GetSpendingSummaryResponse) => void): grpc.ClientUnaryCall; + public recordSpending(request: api_keys_pb.RecordSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + public recordSpending(request: api_keys_pb.RecordSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + public recordSpending(request: api_keys_pb.RecordSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.RecordSpendingResponse) => void): grpc.ClientUnaryCall; + public reverseSpending(request: api_keys_pb.ReverseSpendingRequest, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; + public reverseSpending(request: api_keys_pb.ReverseSpendingRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; + public reverseSpending(request: api_keys_pb.ReverseSpendingRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: api_keys_pb.ReverseSpendingResponse) => void): grpc.ClientUnaryCall; +} diff --git a/core/api/src/services/api-keys/proto/api_keys_grpc_pb.js b/core/api/src/services/api-keys/proto/api_keys_grpc_pb.js new file mode 100644 index 0000000000..c81e5bd9c2 --- /dev/null +++ b/core/api/src/services/api-keys/proto/api_keys_grpc_pb.js @@ -0,0 +1,176 @@ +// GENERATED CODE -- DO NOT EDIT! + +'use strict'; +var grpc = require('@grpc/grpc-js'); +var api_keys_pb = require('./api_keys_pb.js'); + +function serialize_services_api_keys_v1_CheckAndLockSpendingRequest(arg) { + if (!(arg instanceof api_keys_pb.CheckAndLockSpendingRequest)) { + throw new Error('Expected argument of type services.api_keys.v1.CheckAndLockSpendingRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_CheckAndLockSpendingRequest(buffer_arg) { + return api_keys_pb.CheckAndLockSpendingRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_CheckAndLockSpendingResponse(arg) { + if (!(arg instanceof api_keys_pb.CheckAndLockSpendingResponse)) { + throw new Error('Expected argument of type services.api_keys.v1.CheckAndLockSpendingResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_CheckAndLockSpendingResponse(buffer_arg) { + return api_keys_pb.CheckAndLockSpendingResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_CheckSpendingLimitRequest(arg) { + if (!(arg instanceof api_keys_pb.CheckSpendingLimitRequest)) { + throw new Error('Expected argument of type services.api_keys.v1.CheckSpendingLimitRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_CheckSpendingLimitRequest(buffer_arg) { + return api_keys_pb.CheckSpendingLimitRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_CheckSpendingLimitResponse(arg) { + if (!(arg instanceof api_keys_pb.CheckSpendingLimitResponse)) { + throw new Error('Expected argument of type services.api_keys.v1.CheckSpendingLimitResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_CheckSpendingLimitResponse(buffer_arg) { + return api_keys_pb.CheckSpendingLimitResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_GetSpendingSummaryRequest(arg) { + if (!(arg instanceof api_keys_pb.GetSpendingSummaryRequest)) { + throw new Error('Expected argument of type services.api_keys.v1.GetSpendingSummaryRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_GetSpendingSummaryRequest(buffer_arg) { + return api_keys_pb.GetSpendingSummaryRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_GetSpendingSummaryResponse(arg) { + if (!(arg instanceof api_keys_pb.GetSpendingSummaryResponse)) { + throw new Error('Expected argument of type services.api_keys.v1.GetSpendingSummaryResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_GetSpendingSummaryResponse(buffer_arg) { + return api_keys_pb.GetSpendingSummaryResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_RecordSpendingRequest(arg) { + if (!(arg instanceof api_keys_pb.RecordSpendingRequest)) { + throw new Error('Expected argument of type services.api_keys.v1.RecordSpendingRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_RecordSpendingRequest(buffer_arg) { + return api_keys_pb.RecordSpendingRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_RecordSpendingResponse(arg) { + if (!(arg instanceof api_keys_pb.RecordSpendingResponse)) { + throw new Error('Expected argument of type services.api_keys.v1.RecordSpendingResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_RecordSpendingResponse(buffer_arg) { + return api_keys_pb.RecordSpendingResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_ReverseSpendingRequest(arg) { + if (!(arg instanceof api_keys_pb.ReverseSpendingRequest)) { + throw new Error('Expected argument of type services.api_keys.v1.ReverseSpendingRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_ReverseSpendingRequest(buffer_arg) { + return api_keys_pb.ReverseSpendingRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_services_api_keys_v1_ReverseSpendingResponse(arg) { + if (!(arg instanceof api_keys_pb.ReverseSpendingResponse)) { + throw new Error('Expected argument of type services.api_keys.v1.ReverseSpendingResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_services_api_keys_v1_ReverseSpendingResponse(buffer_arg) { + return api_keys_pb.ReverseSpendingResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + + +var ApiKeysServiceService = exports.ApiKeysServiceService = { + checkSpendingLimit: { + path: '/services.api_keys.v1.ApiKeysService/CheckSpendingLimit', + requestStream: false, + responseStream: false, + requestType: api_keys_pb.CheckSpendingLimitRequest, + responseType: api_keys_pb.CheckSpendingLimitResponse, + requestSerialize: serialize_services_api_keys_v1_CheckSpendingLimitRequest, + requestDeserialize: deserialize_services_api_keys_v1_CheckSpendingLimitRequest, + responseSerialize: serialize_services_api_keys_v1_CheckSpendingLimitResponse, + responseDeserialize: deserialize_services_api_keys_v1_CheckSpendingLimitResponse, + }, + checkAndLockSpending: { + path: '/services.api_keys.v1.ApiKeysService/CheckAndLockSpending', + requestStream: false, + responseStream: false, + requestType: api_keys_pb.CheckAndLockSpendingRequest, + responseType: api_keys_pb.CheckAndLockSpendingResponse, + requestSerialize: serialize_services_api_keys_v1_CheckAndLockSpendingRequest, + requestDeserialize: deserialize_services_api_keys_v1_CheckAndLockSpendingRequest, + responseSerialize: serialize_services_api_keys_v1_CheckAndLockSpendingResponse, + responseDeserialize: deserialize_services_api_keys_v1_CheckAndLockSpendingResponse, + }, + getSpendingSummary: { + path: '/services.api_keys.v1.ApiKeysService/GetSpendingSummary', + requestStream: false, + responseStream: false, + requestType: api_keys_pb.GetSpendingSummaryRequest, + responseType: api_keys_pb.GetSpendingSummaryResponse, + requestSerialize: serialize_services_api_keys_v1_GetSpendingSummaryRequest, + requestDeserialize: deserialize_services_api_keys_v1_GetSpendingSummaryRequest, + responseSerialize: serialize_services_api_keys_v1_GetSpendingSummaryResponse, + responseDeserialize: deserialize_services_api_keys_v1_GetSpendingSummaryResponse, + }, + recordSpending: { + path: '/services.api_keys.v1.ApiKeysService/RecordSpending', + requestStream: false, + responseStream: false, + requestType: api_keys_pb.RecordSpendingRequest, + responseType: api_keys_pb.RecordSpendingResponse, + requestSerialize: serialize_services_api_keys_v1_RecordSpendingRequest, + requestDeserialize: deserialize_services_api_keys_v1_RecordSpendingRequest, + responseSerialize: serialize_services_api_keys_v1_RecordSpendingResponse, + responseDeserialize: deserialize_services_api_keys_v1_RecordSpendingResponse, + }, + reverseSpending: { + path: '/services.api_keys.v1.ApiKeysService/ReverseSpending', + requestStream: false, + responseStream: false, + requestType: api_keys_pb.ReverseSpendingRequest, + responseType: api_keys_pb.ReverseSpendingResponse, + requestSerialize: serialize_services_api_keys_v1_ReverseSpendingRequest, + requestDeserialize: deserialize_services_api_keys_v1_ReverseSpendingRequest, + responseSerialize: serialize_services_api_keys_v1_ReverseSpendingResponse, + responseDeserialize: deserialize_services_api_keys_v1_ReverseSpendingResponse, + }, +}; + +exports.ApiKeysServiceClient = grpc.makeGenericClientConstructor(ApiKeysServiceService, 'ApiKeysService'); diff --git a/core/api/src/services/api-keys/proto/api_keys_pb.d.ts b/core/api/src/services/api-keys/proto/api_keys_pb.d.ts new file mode 100644 index 0000000000..8f5ded87cf --- /dev/null +++ b/core/api/src/services/api-keys/proto/api_keys_pb.d.ts @@ -0,0 +1,339 @@ +// package: services.api_keys.v1 +// file: api_keys.proto + +/* tslint:disable */ +/* eslint-disable */ + +import * as jspb from "google-protobuf"; + +export class CheckSpendingLimitRequest extends jspb.Message { + getApiKeyId(): string; + setApiKeyId(value: string): CheckSpendingLimitRequest; + getAmountSats(): number; + setAmountSats(value: number): CheckSpendingLimitRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CheckSpendingLimitRequest.AsObject; + static toObject(includeInstance: boolean, msg: CheckSpendingLimitRequest): CheckSpendingLimitRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CheckSpendingLimitRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CheckSpendingLimitRequest; + static deserializeBinaryFromReader(message: CheckSpendingLimitRequest, reader: jspb.BinaryReader): CheckSpendingLimitRequest; +} + +export namespace CheckSpendingLimitRequest { + export type AsObject = { + apiKeyId: string, + amountSats: number, + } +} + +export class CheckSpendingLimitResponse extends jspb.Message { + getAllowed(): boolean; + setAllowed(value: boolean): CheckSpendingLimitResponse; + + hasDailyLimitSats(): boolean; + clearDailyLimitSats(): void; + getDailyLimitSats(): number | undefined; + setDailyLimitSats(value: number): CheckSpendingLimitResponse; + + hasWeeklyLimitSats(): boolean; + clearWeeklyLimitSats(): void; + getWeeklyLimitSats(): number | undefined; + setWeeklyLimitSats(value: number): CheckSpendingLimitResponse; + + hasMonthlyLimitSats(): boolean; + clearMonthlyLimitSats(): void; + getMonthlyLimitSats(): number | undefined; + setMonthlyLimitSats(value: number): CheckSpendingLimitResponse; + + hasAnnualLimitSats(): boolean; + clearAnnualLimitSats(): void; + getAnnualLimitSats(): number | undefined; + setAnnualLimitSats(value: number): CheckSpendingLimitResponse; + getDailySpentSats(): number; + setDailySpentSats(value: number): CheckSpendingLimitResponse; + getWeeklySpentSats(): number; + setWeeklySpentSats(value: number): CheckSpendingLimitResponse; + getMonthlySpentSats(): number; + setMonthlySpentSats(value: number): CheckSpendingLimitResponse; + getAnnualSpentSats(): number; + setAnnualSpentSats(value: number): CheckSpendingLimitResponse; + + hasRemainingDailySats(): boolean; + clearRemainingDailySats(): void; + getRemainingDailySats(): number | undefined; + setRemainingDailySats(value: number): CheckSpendingLimitResponse; + + hasRemainingWeeklySats(): boolean; + clearRemainingWeeklySats(): void; + getRemainingWeeklySats(): number | undefined; + setRemainingWeeklySats(value: number): CheckSpendingLimitResponse; + + hasRemainingMonthlySats(): boolean; + clearRemainingMonthlySats(): void; + getRemainingMonthlySats(): number | undefined; + setRemainingMonthlySats(value: number): CheckSpendingLimitResponse; + + hasRemainingAnnualSats(): boolean; + clearRemainingAnnualSats(): void; + getRemainingAnnualSats(): number | undefined; + setRemainingAnnualSats(value: number): CheckSpendingLimitResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CheckSpendingLimitResponse.AsObject; + static toObject(includeInstance: boolean, msg: CheckSpendingLimitResponse): CheckSpendingLimitResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CheckSpendingLimitResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CheckSpendingLimitResponse; + static deserializeBinaryFromReader(message: CheckSpendingLimitResponse, reader: jspb.BinaryReader): CheckSpendingLimitResponse; +} + +export namespace CheckSpendingLimitResponse { + export type AsObject = { + allowed: boolean, + dailyLimitSats?: number, + weeklyLimitSats?: number, + monthlyLimitSats?: number, + annualLimitSats?: number, + dailySpentSats: number, + weeklySpentSats: number, + monthlySpentSats: number, + annualSpentSats: number, + remainingDailySats?: number, + remainingWeeklySats?: number, + remainingMonthlySats?: number, + remainingAnnualSats?: number, + } +} + +export class CheckAndLockSpendingRequest extends jspb.Message { + getApiKeyId(): string; + setApiKeyId(value: string): CheckAndLockSpendingRequest; + getAmountSats(): number; + setAmountSats(value: number): CheckAndLockSpendingRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CheckAndLockSpendingRequest.AsObject; + static toObject(includeInstance: boolean, msg: CheckAndLockSpendingRequest): CheckAndLockSpendingRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CheckAndLockSpendingRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CheckAndLockSpendingRequest; + static deserializeBinaryFromReader(message: CheckAndLockSpendingRequest, reader: jspb.BinaryReader): CheckAndLockSpendingRequest; +} + +export namespace CheckAndLockSpendingRequest { + export type AsObject = { + apiKeyId: string, + amountSats: number, + } +} + +export class CheckAndLockSpendingResponse extends jspb.Message { + getEphemeralId(): string; + setEphemeralId(value: string): CheckAndLockSpendingResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CheckAndLockSpendingResponse.AsObject; + static toObject(includeInstance: boolean, msg: CheckAndLockSpendingResponse): CheckAndLockSpendingResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CheckAndLockSpendingResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CheckAndLockSpendingResponse; + static deserializeBinaryFromReader(message: CheckAndLockSpendingResponse, reader: jspb.BinaryReader): CheckAndLockSpendingResponse; +} + +export namespace CheckAndLockSpendingResponse { + export type AsObject = { + ephemeralId: string, + } +} + +export class GetSpendingSummaryRequest extends jspb.Message { + getApiKeyId(): string; + setApiKeyId(value: string): GetSpendingSummaryRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): GetSpendingSummaryRequest.AsObject; + static toObject(includeInstance: boolean, msg: GetSpendingSummaryRequest): GetSpendingSummaryRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: GetSpendingSummaryRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): GetSpendingSummaryRequest; + static deserializeBinaryFromReader(message: GetSpendingSummaryRequest, reader: jspb.BinaryReader): GetSpendingSummaryRequest; +} + +export namespace GetSpendingSummaryRequest { + export type AsObject = { + apiKeyId: string, + } +} + +export class GetSpendingSummaryResponse extends jspb.Message { + + hasDailyLimitSats(): boolean; + clearDailyLimitSats(): void; + getDailyLimitSats(): number | undefined; + setDailyLimitSats(value: number): GetSpendingSummaryResponse; + + hasWeeklyLimitSats(): boolean; + clearWeeklyLimitSats(): void; + getWeeklyLimitSats(): number | undefined; + setWeeklyLimitSats(value: number): GetSpendingSummaryResponse; + + hasMonthlyLimitSats(): boolean; + clearMonthlyLimitSats(): void; + getMonthlyLimitSats(): number | undefined; + setMonthlyLimitSats(value: number): GetSpendingSummaryResponse; + + hasAnnualLimitSats(): boolean; + clearAnnualLimitSats(): void; + getAnnualLimitSats(): number | undefined; + setAnnualLimitSats(value: number): GetSpendingSummaryResponse; + getDailySpentSats(): number; + setDailySpentSats(value: number): GetSpendingSummaryResponse; + getWeeklySpentSats(): number; + setWeeklySpentSats(value: number): GetSpendingSummaryResponse; + getMonthlySpentSats(): number; + setMonthlySpentSats(value: number): GetSpendingSummaryResponse; + getAnnualSpentSats(): number; + setAnnualSpentSats(value: number): GetSpendingSummaryResponse; + + hasRemainingDailySats(): boolean; + clearRemainingDailySats(): void; + getRemainingDailySats(): number | undefined; + setRemainingDailySats(value: number): GetSpendingSummaryResponse; + + hasRemainingWeeklySats(): boolean; + clearRemainingWeeklySats(): void; + getRemainingWeeklySats(): number | undefined; + setRemainingWeeklySats(value: number): GetSpendingSummaryResponse; + + hasRemainingMonthlySats(): boolean; + clearRemainingMonthlySats(): void; + getRemainingMonthlySats(): number | undefined; + setRemainingMonthlySats(value: number): GetSpendingSummaryResponse; + + hasRemainingAnnualSats(): boolean; + clearRemainingAnnualSats(): void; + getRemainingAnnualSats(): number | undefined; + setRemainingAnnualSats(value: number): GetSpendingSummaryResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): GetSpendingSummaryResponse.AsObject; + static toObject(includeInstance: boolean, msg: GetSpendingSummaryResponse): GetSpendingSummaryResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: GetSpendingSummaryResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): GetSpendingSummaryResponse; + static deserializeBinaryFromReader(message: GetSpendingSummaryResponse, reader: jspb.BinaryReader): GetSpendingSummaryResponse; +} + +export namespace GetSpendingSummaryResponse { + export type AsObject = { + dailyLimitSats?: number, + weeklyLimitSats?: number, + monthlyLimitSats?: number, + annualLimitSats?: number, + dailySpentSats: number, + weeklySpentSats: number, + monthlySpentSats: number, + annualSpentSats: number, + remainingDailySats?: number, + remainingWeeklySats?: number, + remainingMonthlySats?: number, + remainingAnnualSats?: number, + } +} + +export class RecordSpendingRequest extends jspb.Message { + getApiKeyId(): string; + setApiKeyId(value: string): RecordSpendingRequest; + getAmountSats(): number; + setAmountSats(value: number): RecordSpendingRequest; + + hasTransactionId(): boolean; + clearTransactionId(): void; + getTransactionId(): string | undefined; + setTransactionId(value: string): RecordSpendingRequest; + + hasEphemeralId(): boolean; + clearEphemeralId(): void; + getEphemeralId(): string | undefined; + setEphemeralId(value: string): RecordSpendingRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RecordSpendingRequest.AsObject; + static toObject(includeInstance: boolean, msg: RecordSpendingRequest): RecordSpendingRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RecordSpendingRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RecordSpendingRequest; + static deserializeBinaryFromReader(message: RecordSpendingRequest, reader: jspb.BinaryReader): RecordSpendingRequest; +} + +export namespace RecordSpendingRequest { + export type AsObject = { + apiKeyId: string, + amountSats: number, + transactionId?: string, + ephemeralId?: string, + } +} + +export class RecordSpendingResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RecordSpendingResponse.AsObject; + static toObject(includeInstance: boolean, msg: RecordSpendingResponse): RecordSpendingResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RecordSpendingResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RecordSpendingResponse; + static deserializeBinaryFromReader(message: RecordSpendingResponse, reader: jspb.BinaryReader): RecordSpendingResponse; +} + +export namespace RecordSpendingResponse { + export type AsObject = { + } +} + +export class ReverseSpendingRequest extends jspb.Message { + getTransactionId(): string; + setTransactionId(value: string): ReverseSpendingRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ReverseSpendingRequest.AsObject; + static toObject(includeInstance: boolean, msg: ReverseSpendingRequest): ReverseSpendingRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ReverseSpendingRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ReverseSpendingRequest; + static deserializeBinaryFromReader(message: ReverseSpendingRequest, reader: jspb.BinaryReader): ReverseSpendingRequest; +} + +export namespace ReverseSpendingRequest { + export type AsObject = { + transactionId: string, + } +} + +export class ReverseSpendingResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ReverseSpendingResponse.AsObject; + static toObject(includeInstance: boolean, msg: ReverseSpendingResponse): ReverseSpendingResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ReverseSpendingResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ReverseSpendingResponse; + static deserializeBinaryFromReader(message: ReverseSpendingResponse, reader: jspb.BinaryReader): ReverseSpendingResponse; +} + +export namespace ReverseSpendingResponse { + export type AsObject = { + } +} diff --git a/core/api/src/services/api-keys/proto/api_keys_pb.js b/core/api/src/services/api-keys/proto/api_keys_pb.js new file mode 100644 index 0000000000..d665051bd6 --- /dev/null +++ b/core/api/src/services/api-keys/proto/api_keys_pb.js @@ -0,0 +1,2650 @@ +// source: api_keys.proto +/** + * @fileoverview + * @enhanceable + * @suppress {missingRequire} reports error on implicit type usages. + * @suppress {messageConventions} JS Compiler reports an error if a variable or + * field starts with 'MSG_' and isn't a translatable message. + * @public + */ +// GENERATED CODE -- DO NOT EDIT! +/* eslint-disable */ +// @ts-nocheck + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = + (typeof globalThis !== 'undefined' && globalThis) || + (typeof window !== 'undefined' && window) || + (typeof global !== 'undefined' && global) || + (typeof self !== 'undefined' && self) || + (function () { return this; }).call(null) || + Function('return this')(); + +goog.exportSymbol('proto.services.api_keys.v1.CheckAndLockSpendingRequest', null, global); +goog.exportSymbol('proto.services.api_keys.v1.CheckAndLockSpendingResponse', null, global); +goog.exportSymbol('proto.services.api_keys.v1.CheckSpendingLimitRequest', null, global); +goog.exportSymbol('proto.services.api_keys.v1.CheckSpendingLimitResponse', null, global); +goog.exportSymbol('proto.services.api_keys.v1.GetSpendingSummaryRequest', null, global); +goog.exportSymbol('proto.services.api_keys.v1.GetSpendingSummaryResponse', null, global); +goog.exportSymbol('proto.services.api_keys.v1.RecordSpendingRequest', null, global); +goog.exportSymbol('proto.services.api_keys.v1.RecordSpendingResponse', null, global); +goog.exportSymbol('proto.services.api_keys.v1.ReverseSpendingRequest', null, global); +goog.exportSymbol('proto.services.api_keys.v1.ReverseSpendingResponse', null, global); +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.CheckSpendingLimitRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.CheckSpendingLimitRequest.displayName = 'proto.services.api_keys.v1.CheckSpendingLimitRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.CheckSpendingLimitResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.CheckSpendingLimitResponse.displayName = 'proto.services.api_keys.v1.CheckSpendingLimitResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.CheckAndLockSpendingRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.CheckAndLockSpendingRequest.displayName = 'proto.services.api_keys.v1.CheckAndLockSpendingRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.CheckAndLockSpendingResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.CheckAndLockSpendingResponse.displayName = 'proto.services.api_keys.v1.CheckAndLockSpendingResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.GetSpendingSummaryRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.GetSpendingSummaryRequest.displayName = 'proto.services.api_keys.v1.GetSpendingSummaryRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.GetSpendingSummaryResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.GetSpendingSummaryResponse.displayName = 'proto.services.api_keys.v1.GetSpendingSummaryResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.RecordSpendingRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.RecordSpendingRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.RecordSpendingRequest.displayName = 'proto.services.api_keys.v1.RecordSpendingRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.RecordSpendingResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.RecordSpendingResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.RecordSpendingResponse.displayName = 'proto.services.api_keys.v1.RecordSpendingResponse'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.ReverseSpendingRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.ReverseSpendingRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.ReverseSpendingRequest.displayName = 'proto.services.api_keys.v1.ReverseSpendingRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.services.api_keys.v1.ReverseSpendingResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.services.api_keys.v1.ReverseSpendingResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.services.api_keys.v1.ReverseSpendingResponse.displayName = 'proto.services.api_keys.v1.ReverseSpendingResponse'; +} + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.CheckSpendingLimitRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.CheckSpendingLimitRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.toObject = function(includeInstance, msg) { + var f, obj = { +apiKeyId: jspb.Message.getFieldWithDefault(msg, 1, ""), +amountSats: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitRequest} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.CheckSpendingLimitRequest; + return proto.services.api_keys.v1.CheckSpendingLimitRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.CheckSpendingLimitRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitRequest} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setApiKeyId(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAmountSats(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.CheckSpendingLimitRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.CheckSpendingLimitRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getApiKeyId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getAmountSats(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } +}; + + +/** + * optional string api_key_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.getApiKeyId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitRequest} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.setApiKeyId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional int64 amount_sats = 2; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.getAmountSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitRequest} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitRequest.prototype.setAmountSats = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.CheckSpendingLimitResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.CheckSpendingLimitResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.toObject = function(includeInstance, msg) { + var f, obj = { +allowed: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), +dailyLimitSats: (f = jspb.Message.getField(msg, 2)) == null ? undefined : f, +weeklyLimitSats: (f = jspb.Message.getField(msg, 3)) == null ? undefined : f, +monthlyLimitSats: (f = jspb.Message.getField(msg, 4)) == null ? undefined : f, +annualLimitSats: (f = jspb.Message.getField(msg, 5)) == null ? undefined : f, +dailySpentSats: jspb.Message.getFieldWithDefault(msg, 6, 0), +weeklySpentSats: jspb.Message.getFieldWithDefault(msg, 7, 0), +monthlySpentSats: jspb.Message.getFieldWithDefault(msg, 8, 0), +annualSpentSats: jspb.Message.getFieldWithDefault(msg, 9, 0), +remainingDailySats: (f = jspb.Message.getField(msg, 10)) == null ? undefined : f, +remainingWeeklySats: (f = jspb.Message.getField(msg, 11)) == null ? undefined : f, +remainingMonthlySats: (f = jspb.Message.getField(msg, 12)) == null ? undefined : f, +remainingAnnualSats: (f = jspb.Message.getField(msg, 13)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.CheckSpendingLimitResponse; + return proto.services.api_keys.v1.CheckSpendingLimitResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.CheckSpendingLimitResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setAllowed(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setDailyLimitSats(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setWeeklyLimitSats(value); + break; + case 4: + var value = /** @type {number} */ (reader.readInt64()); + msg.setMonthlyLimitSats(value); + break; + case 5: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAnnualLimitSats(value); + break; + case 6: + var value = /** @type {number} */ (reader.readInt64()); + msg.setDailySpentSats(value); + break; + case 7: + var value = /** @type {number} */ (reader.readInt64()); + msg.setWeeklySpentSats(value); + break; + case 8: + var value = /** @type {number} */ (reader.readInt64()); + msg.setMonthlySpentSats(value); + break; + case 9: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAnnualSpentSats(value); + break; + case 10: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingDailySats(value); + break; + case 11: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingWeeklySats(value); + break; + case 12: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingMonthlySats(value); + break; + case 13: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingAnnualSats(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.CheckSpendingLimitResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.CheckSpendingLimitResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAllowed(); + if (f) { + writer.writeBool( + 1, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeInt64( + 2, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeInt64( + 3, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeInt64( + 4, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 5)); + if (f != null) { + writer.writeInt64( + 5, + f + ); + } + f = message.getDailySpentSats(); + if (f !== 0) { + writer.writeInt64( + 6, + f + ); + } + f = message.getWeeklySpentSats(); + if (f !== 0) { + writer.writeInt64( + 7, + f + ); + } + f = message.getMonthlySpentSats(); + if (f !== 0) { + writer.writeInt64( + 8, + f + ); + } + f = message.getAnnualSpentSats(); + if (f !== 0) { + writer.writeInt64( + 9, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 10)); + if (f != null) { + writer.writeInt64( + 10, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 11)); + if (f != null) { + writer.writeInt64( + 11, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 12)); + if (f != null) { + writer.writeInt64( + 12, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 13)); + if (f != null) { + writer.writeInt64( + 13, + f + ); + } +}; + + +/** + * optional bool allowed = 1; + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getAllowed = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setAllowed = function(value) { + return jspb.Message.setProto3BooleanField(this, 1, value); +}; + + +/** + * optional int64 daily_limit_sats = 2; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getDailyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setDailyLimitSats = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearDailyLimitSats = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasDailyLimitSats = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional int64 weekly_limit_sats = 3; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getWeeklyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setWeeklyLimitSats = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearWeeklyLimitSats = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasWeeklyLimitSats = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional int64 monthly_limit_sats = 4; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getMonthlyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setMonthlyLimitSats = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearMonthlyLimitSats = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasMonthlyLimitSats = function() { + return jspb.Message.getField(this, 4) != null; +}; + + +/** + * optional int64 annual_limit_sats = 5; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getAnnualLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setAnnualLimitSats = function(value) { + return jspb.Message.setField(this, 5, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearAnnualLimitSats = function() { + return jspb.Message.setField(this, 5, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasAnnualLimitSats = function() { + return jspb.Message.getField(this, 5) != null; +}; + + +/** + * optional int64 daily_spent_sats = 6; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getDailySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setDailySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 6, value); +}; + + +/** + * optional int64 weekly_spent_sats = 7; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getWeeklySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 7, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setWeeklySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 7, value); +}; + + +/** + * optional int64 monthly_spent_sats = 8; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getMonthlySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 8, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setMonthlySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 8, value); +}; + + +/** + * optional int64 annual_spent_sats = 9; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getAnnualSpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 9, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setAnnualSpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 9, value); +}; + + +/** + * optional int64 remaining_daily_sats = 10; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getRemainingDailySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 10, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setRemainingDailySats = function(value) { + return jspb.Message.setField(this, 10, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearRemainingDailySats = function() { + return jspb.Message.setField(this, 10, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasRemainingDailySats = function() { + return jspb.Message.getField(this, 10) != null; +}; + + +/** + * optional int64 remaining_weekly_sats = 11; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getRemainingWeeklySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 11, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setRemainingWeeklySats = function(value) { + return jspb.Message.setField(this, 11, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearRemainingWeeklySats = function() { + return jspb.Message.setField(this, 11, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasRemainingWeeklySats = function() { + return jspb.Message.getField(this, 11) != null; +}; + + +/** + * optional int64 remaining_monthly_sats = 12; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getRemainingMonthlySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 12, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setRemainingMonthlySats = function(value) { + return jspb.Message.setField(this, 12, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearRemainingMonthlySats = function() { + return jspb.Message.setField(this, 12, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasRemainingMonthlySats = function() { + return jspb.Message.getField(this, 12) != null; +}; + + +/** + * optional int64 remaining_annual_sats = 13; + * @return {number} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.getRemainingAnnualSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 13, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.setRemainingAnnualSats = function(value) { + return jspb.Message.setField(this, 13, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.CheckSpendingLimitResponse} returns this + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.clearRemainingAnnualSats = function() { + return jspb.Message.setField(this, 13, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.CheckSpendingLimitResponse.prototype.hasRemainingAnnualSats = function() { + return jspb.Message.getField(this, 13) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.CheckAndLockSpendingRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.toObject = function(includeInstance, msg) { + var f, obj = { +apiKeyId: jspb.Message.getFieldWithDefault(msg, 1, ""), +amountSats: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.CheckAndLockSpendingRequest; + return proto.services.api_keys.v1.CheckAndLockSpendingRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setApiKeyId(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAmountSats(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.CheckAndLockSpendingRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getApiKeyId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getAmountSats(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } +}; + + +/** + * optional string api_key_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.getApiKeyId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} returns this + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.setApiKeyId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional int64 amount_sats = 2; + * @return {number} + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.getAmountSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingRequest} returns this + */ +proto.services.api_keys.v1.CheckAndLockSpendingRequest.prototype.setAmountSats = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.CheckAndLockSpendingResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.toObject = function(includeInstance, msg) { + var f, obj = { +ephemeralId: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.CheckAndLockSpendingResponse; + return proto.services.api_keys.v1.CheckAndLockSpendingResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setEphemeralId(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.CheckAndLockSpendingResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getEphemeralId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string ephemeral_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.prototype.getEphemeralId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.CheckAndLockSpendingResponse} returns this + */ +proto.services.api_keys.v1.CheckAndLockSpendingResponse.prototype.setEphemeralId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.GetSpendingSummaryRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.GetSpendingSummaryRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.toObject = function(includeInstance, msg) { + var f, obj = { +apiKeyId: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryRequest} + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.GetSpendingSummaryRequest; + return proto.services.api_keys.v1.GetSpendingSummaryRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.GetSpendingSummaryRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryRequest} + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setApiKeyId(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.GetSpendingSummaryRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.GetSpendingSummaryRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getApiKeyId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string api_key_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.prototype.getApiKeyId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryRequest} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryRequest.prototype.setApiKeyId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.GetSpendingSummaryResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.GetSpendingSummaryResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.toObject = function(includeInstance, msg) { + var f, obj = { +dailyLimitSats: (f = jspb.Message.getField(msg, 1)) == null ? undefined : f, +weeklyLimitSats: (f = jspb.Message.getField(msg, 2)) == null ? undefined : f, +monthlyLimitSats: (f = jspb.Message.getField(msg, 3)) == null ? undefined : f, +annualLimitSats: (f = jspb.Message.getField(msg, 4)) == null ? undefined : f, +dailySpentSats: jspb.Message.getFieldWithDefault(msg, 5, 0), +weeklySpentSats: jspb.Message.getFieldWithDefault(msg, 6, 0), +monthlySpentSats: jspb.Message.getFieldWithDefault(msg, 7, 0), +annualSpentSats: jspb.Message.getFieldWithDefault(msg, 8, 0), +remainingDailySats: (f = jspb.Message.getField(msg, 9)) == null ? undefined : f, +remainingWeeklySats: (f = jspb.Message.getField(msg, 10)) == null ? undefined : f, +remainingMonthlySats: (f = jspb.Message.getField(msg, 11)) == null ? undefined : f, +remainingAnnualSats: (f = jspb.Message.getField(msg, 12)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.GetSpendingSummaryResponse; + return proto.services.api_keys.v1.GetSpendingSummaryResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.GetSpendingSummaryResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readInt64()); + msg.setDailyLimitSats(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setWeeklyLimitSats(value); + break; + case 3: + var value = /** @type {number} */ (reader.readInt64()); + msg.setMonthlyLimitSats(value); + break; + case 4: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAnnualLimitSats(value); + break; + case 5: + var value = /** @type {number} */ (reader.readInt64()); + msg.setDailySpentSats(value); + break; + case 6: + var value = /** @type {number} */ (reader.readInt64()); + msg.setWeeklySpentSats(value); + break; + case 7: + var value = /** @type {number} */ (reader.readInt64()); + msg.setMonthlySpentSats(value); + break; + case 8: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAnnualSpentSats(value); + break; + case 9: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingDailySats(value); + break; + case 10: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingWeeklySats(value); + break; + case 11: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingMonthlySats(value); + break; + case 12: + var value = /** @type {number} */ (reader.readInt64()); + msg.setRemainingAnnualSats(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.GetSpendingSummaryResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.GetSpendingSummaryResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = /** @type {number} */ (jspb.Message.getField(message, 1)); + if (f != null) { + writer.writeInt64( + 1, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeInt64( + 2, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeInt64( + 3, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeInt64( + 4, + f + ); + } + f = message.getDailySpentSats(); + if (f !== 0) { + writer.writeInt64( + 5, + f + ); + } + f = message.getWeeklySpentSats(); + if (f !== 0) { + writer.writeInt64( + 6, + f + ); + } + f = message.getMonthlySpentSats(); + if (f !== 0) { + writer.writeInt64( + 7, + f + ); + } + f = message.getAnnualSpentSats(); + if (f !== 0) { + writer.writeInt64( + 8, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 9)); + if (f != null) { + writer.writeInt64( + 9, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 10)); + if (f != null) { + writer.writeInt64( + 10, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 11)); + if (f != null) { + writer.writeInt64( + 11, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 12)); + if (f != null) { + writer.writeInt64( + 12, + f + ); + } +}; + + +/** + * optional int64 daily_limit_sats = 1; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getDailyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setDailyLimitSats = function(value) { + return jspb.Message.setField(this, 1, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearDailyLimitSats = function() { + return jspb.Message.setField(this, 1, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasDailyLimitSats = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional int64 weekly_limit_sats = 2; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getWeeklyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setWeeklyLimitSats = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearWeeklyLimitSats = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasWeeklyLimitSats = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * optional int64 monthly_limit_sats = 3; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getMonthlyLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setMonthlyLimitSats = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearMonthlyLimitSats = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasMonthlyLimitSats = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional int64 annual_limit_sats = 4; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getAnnualLimitSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setAnnualLimitSats = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearAnnualLimitSats = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasAnnualLimitSats = function() { + return jspb.Message.getField(this, 4) != null; +}; + + +/** + * optional int64 daily_spent_sats = 5; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getDailySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setDailySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 5, value); +}; + + +/** + * optional int64 weekly_spent_sats = 6; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getWeeklySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setWeeklySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 6, value); +}; + + +/** + * optional int64 monthly_spent_sats = 7; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getMonthlySpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 7, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setMonthlySpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 7, value); +}; + + +/** + * optional int64 annual_spent_sats = 8; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getAnnualSpentSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 8, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setAnnualSpentSats = function(value) { + return jspb.Message.setProto3IntField(this, 8, value); +}; + + +/** + * optional int64 remaining_daily_sats = 9; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getRemainingDailySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 9, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setRemainingDailySats = function(value) { + return jspb.Message.setField(this, 9, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearRemainingDailySats = function() { + return jspb.Message.setField(this, 9, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasRemainingDailySats = function() { + return jspb.Message.getField(this, 9) != null; +}; + + +/** + * optional int64 remaining_weekly_sats = 10; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getRemainingWeeklySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 10, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setRemainingWeeklySats = function(value) { + return jspb.Message.setField(this, 10, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearRemainingWeeklySats = function() { + return jspb.Message.setField(this, 10, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasRemainingWeeklySats = function() { + return jspb.Message.getField(this, 10) != null; +}; + + +/** + * optional int64 remaining_monthly_sats = 11; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getRemainingMonthlySats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 11, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setRemainingMonthlySats = function(value) { + return jspb.Message.setField(this, 11, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearRemainingMonthlySats = function() { + return jspb.Message.setField(this, 11, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasRemainingMonthlySats = function() { + return jspb.Message.getField(this, 11) != null; +}; + + +/** + * optional int64 remaining_annual_sats = 12; + * @return {number} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.getRemainingAnnualSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 12, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.setRemainingAnnualSats = function(value) { + return jspb.Message.setField(this, 12, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.GetSpendingSummaryResponse} returns this + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.clearRemainingAnnualSats = function() { + return jspb.Message.setField(this, 12, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.GetSpendingSummaryResponse.prototype.hasRemainingAnnualSats = function() { + return jspb.Message.getField(this, 12) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.RecordSpendingRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.RecordSpendingRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.RecordSpendingRequest.toObject = function(includeInstance, msg) { + var f, obj = { +apiKeyId: jspb.Message.getFieldWithDefault(msg, 1, ""), +amountSats: jspb.Message.getFieldWithDefault(msg, 2, 0), +transactionId: (f = jspb.Message.getField(msg, 3)) == null ? undefined : f, +ephemeralId: (f = jspb.Message.getField(msg, 4)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} + */ +proto.services.api_keys.v1.RecordSpendingRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.RecordSpendingRequest; + return proto.services.api_keys.v1.RecordSpendingRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.RecordSpendingRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} + */ +proto.services.api_keys.v1.RecordSpendingRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setApiKeyId(value); + break; + case 2: + var value = /** @type {number} */ (reader.readInt64()); + msg.setAmountSats(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setTransactionId(value); + break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setEphemeralId(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.RecordSpendingRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.RecordSpendingRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.RecordSpendingRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getApiKeyId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getAmountSats(); + if (f !== 0) { + writer.writeInt64( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeString( + 3, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeString( + 4, + f + ); + } +}; + + +/** + * optional string api_key_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.getApiKeyId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.setApiKeyId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional int64 amount_sats = 2; + * @return {number} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.getAmountSats = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.setAmountSats = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + +/** + * optional string transaction_id = 3; + * @return {string} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.getTransactionId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.setTransactionId = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.clearTransactionId = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.hasTransactionId = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional string ephemeral_id = 4; + * @return {string} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.getEphemeralId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.setEphemeralId = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.services.api_keys.v1.RecordSpendingRequest} returns this + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.clearEphemeralId = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.services.api_keys.v1.RecordSpendingRequest.prototype.hasEphemeralId = function() { + return jspb.Message.getField(this, 4) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.RecordSpendingResponse.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.RecordSpendingResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.RecordSpendingResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.RecordSpendingResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.RecordSpendingResponse} + */ +proto.services.api_keys.v1.RecordSpendingResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.RecordSpendingResponse; + return proto.services.api_keys.v1.RecordSpendingResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.RecordSpendingResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.RecordSpendingResponse} + */ +proto.services.api_keys.v1.RecordSpendingResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.RecordSpendingResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.RecordSpendingResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.RecordSpendingResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.RecordSpendingResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.ReverseSpendingRequest.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.ReverseSpendingRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.ReverseSpendingRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.ReverseSpendingRequest.toObject = function(includeInstance, msg) { + var f, obj = { +transactionId: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.ReverseSpendingRequest} + */ +proto.services.api_keys.v1.ReverseSpendingRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.ReverseSpendingRequest; + return proto.services.api_keys.v1.ReverseSpendingRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.ReverseSpendingRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.ReverseSpendingRequest} + */ +proto.services.api_keys.v1.ReverseSpendingRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setTransactionId(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.ReverseSpendingRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.ReverseSpendingRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.ReverseSpendingRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.ReverseSpendingRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getTransactionId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string transaction_id = 1; + * @return {string} + */ +proto.services.api_keys.v1.ReverseSpendingRequest.prototype.getTransactionId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.services.api_keys.v1.ReverseSpendingRequest} returns this + */ +proto.services.api_keys.v1.ReverseSpendingRequest.prototype.setTransactionId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.services.api_keys.v1.ReverseSpendingResponse.prototype.toObject = function(opt_includeInstance) { + return proto.services.api_keys.v1.ReverseSpendingResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.services.api_keys.v1.ReverseSpendingResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.ReverseSpendingResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.services.api_keys.v1.ReverseSpendingResponse} + */ +proto.services.api_keys.v1.ReverseSpendingResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.services.api_keys.v1.ReverseSpendingResponse; + return proto.services.api_keys.v1.ReverseSpendingResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.services.api_keys.v1.ReverseSpendingResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.services.api_keys.v1.ReverseSpendingResponse} + */ +proto.services.api_keys.v1.ReverseSpendingResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.services.api_keys.v1.ReverseSpendingResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.services.api_keys.v1.ReverseSpendingResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.services.api_keys.v1.ReverseSpendingResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.services.api_keys.v1.ReverseSpendingResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + +goog.object.extend(exports, proto.services.api_keys.v1); diff --git a/core/api/src/services/api-keys/proto/buf.gen.yaml b/core/api/src/services/api-keys/proto/buf.gen.yaml new file mode 100644 index 0000000000..4afda94f7f --- /dev/null +++ b/core/api/src/services/api-keys/proto/buf.gen.yaml @@ -0,0 +1,15 @@ +# /proto/buf.gen.yaml +version: v1 + +plugins: + - name: js + out: . + opt: import_style=commonjs,binary + - name: grpc + out: . + opt: grpc_js + path: grpc_tools_node_protoc_plugin + - name: ts + out: . + opt: grpc_js + path: protoc-gen-ts diff --git a/core/api/test/unit/app/payments/api-key-spending.spec.ts b/core/api/test/unit/app/payments/api-key-spending.spec.ts new file mode 100644 index 0000000000..49b17cba23 --- /dev/null +++ b/core/api/test/unit/app/payments/api-key-spending.spec.ts @@ -0,0 +1,103 @@ +jest.mock("@/services/tracing", () => ({ + recordExceptionInCurrentSpan: jest.fn(), +})) + +jest.mock("@/services/api-keys", () => ({ + __mockApiKeys: { + checkAndLockSpending: jest.fn(), + recordSpending: jest.fn(), + reverseSpending: jest.fn(), + }, + ApiKeysService: () => ({ + checkAndLockSpending: + jest.requireMock("@/services/api-keys").__mockApiKeys.checkAndLockSpending, + recordSpending: jest.requireMock("@/services/api-keys").__mockApiKeys.recordSpending, + reverseSpending: + jest.requireMock("@/services/api-keys").__mockApiKeys.reverseSpending, + }), +})) + +import { + lockApiKeySpending, + recordApiKeySpendingSettlement, + reverseApiKeySpendingSettlement, + settleApiKeySpending, +} from "@/app/payments/api-key-spending" + +const mockApiKeys = jest.requireMock("@/services/api-keys").__mockApiKeys as { + checkAndLockSpending: jest.Mock + recordSpending: jest.Mock + reverseSpending: jest.Mock +} + +describe("api-key-spending", () => { + const apiKeyId = "api-key-id" as ApiKeyId + const amount = { amount: 1000n, currency: "BTC" } as BtcPaymentAmount + const ephemeralId = "ephemeral-id" as EphemeralId + const journalId = "journal-id" as LedgerJournalId + + const lock = { apiKeyId, amount, ephemeralId } + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("lockApiKeySpending", () => { + it("returns undefined when apiKeyId is not provided", async () => { + const result = await lockApiKeySpending({ amount }) + expect(result).toBeUndefined() + expect(mockApiKeys.checkAndLockSpending).not.toHaveBeenCalled() + }) + + it("returns a lock when checkAndLockSpending succeeds", async () => { + mockApiKeys.checkAndLockSpending.mockResolvedValue(ephemeralId) + + const result = await lockApiKeySpending({ apiKeyId, amount }) + + expect(mockApiKeys.checkAndLockSpending).toHaveBeenCalledWith({ apiKeyId, amount }) + expect(result).toEqual({ apiKeyId, amount, ephemeralId }) + }) + + it("returns error when checkAndLockSpending fails", async () => { + const error = new Error("lock failed") + mockApiKeys.checkAndLockSpending.mockResolvedValue(error) + + const result = await lockApiKeySpending({ apiKeyId, amount }) + + expect(result).toBe(error) + }) + }) + + describe("settleApiKeySpending", () => { + it("records spending for record settlement", async () => { + mockApiKeys.recordSpending.mockResolvedValue(true) + + await settleApiKeySpending({ + lock, + settlement: recordApiKeySpendingSettlement(journalId), + }) + + expect(mockApiKeys.recordSpending).toHaveBeenCalledWith({ + apiKeyId, + amount, + transactionId: journalId, + ephemeralId, + }) + expect(mockApiKeys.reverseSpending).not.toHaveBeenCalled() + }) + + it("reverses spending for reverse settlement", async () => { + mockApiKeys.reverseSpending.mockResolvedValue(true) + + await settleApiKeySpending({ + lock, + settlement: reverseApiKeySpendingSettlement(), + }) + + expect(mockApiKeys.reverseSpending).toHaveBeenCalledWith({ + transactionId: ephemeralId, + }) + expect(mockApiKeys.recordSpending).not.toHaveBeenCalled() + }) + }) +}) diff --git a/core/api/test/unit/app/payments/spending-limits.spec.ts b/core/api/test/unit/app/payments/spending-limits.spec.ts new file mode 100644 index 0000000000..104de2535e --- /dev/null +++ b/core/api/test/unit/app/payments/spending-limits.spec.ts @@ -0,0 +1,334 @@ +jest.mock("@/services/tracing", () => ({ + recordExceptionInCurrentSpan: jest.fn(), +})) + +jest.mock("@/services/api-keys", () => ({ + __mockApiKeys: { + checkAndLockSpending: jest.fn(), + recordSpending: jest.fn(), + reverseSpending: jest.fn(), + }, + ApiKeysService: () => ({ + checkAndLockSpending: + jest.requireMock("@/services/api-keys").__mockApiKeys.checkAndLockSpending, + recordSpending: jest.requireMock("@/services/api-keys").__mockApiKeys.recordSpending, + reverseSpending: + jest.requireMock("@/services/api-keys").__mockApiKeys.reverseSpending, + }), +})) + +jest.mock("@/app/accounts", () => ({ + checkIntraledgerLimits: jest.fn(), + checkTradeIntraAccountLimits: jest.fn(), + checkWithdrawalLimits: jest.fn(), +})) + +import { ApiKeySpendingSettlementType } from "@/app/payments/api-key-spending" +import { + recordSettlement, + reverseSettlement, + withSpendingLimits, +} from "@/app/payments/spending-limits" +import { PaymentSendStatus } from "@/domain/bitcoin/lightning" +import { ApiKeyLimitExceededError } from "@/domain/api-keys/errors" +import { CouldNotFindError } from "@/domain/errors" +import { ErrorLevel } from "@/domain/shared" +import { SettlementMethod } from "@/domain/wallets" +import { recordExceptionInCurrentSpan } from "@/services/tracing" +import { + checkIntraledgerLimits, + checkTradeIntraAccountLimits, + checkWithdrawalLimits, +} from "@/app/accounts" + +const mockApiKeys = jest.requireMock("@/services/api-keys").__mockApiKeys as { + checkAndLockSpending: jest.Mock + recordSpending: jest.Mock + reverseSpending: jest.Mock +} + +const mockRecordExceptionInCurrentSpan = recordExceptionInCurrentSpan as jest.Mock +const mockCheckIntraledgerLimits = checkIntraledgerLimits as jest.Mock +const mockCheckTradeIntraAccountLimits = checkTradeIntraAccountLimits as jest.Mock +const mockCheckWithdrawalLimits = checkWithdrawalLimits as jest.Mock + +describe("withSpendingLimits", () => { + const apiKeyId = "api-key-id" as ApiKeyId + const btcPaymentAmount = { amount: 1000n, currency: "BTC" } as BtcPaymentAmount + const journalId = "journal-id" as LedgerJournalId + const walletId = "wallet-id" as WalletId + + const paymentSendSuccessResult: PaymentSendResult = { + status: PaymentSendStatus.Success, + transaction: { walletId } as WalletTransaction, + } + + const paymentSendAlreadyPaidResult: PaymentSendResult = { + status: PaymentSendStatus.AlreadyPaid, + transaction: { walletId } as WalletTransaction, + } + + const paymentSendFailureResult: PaymentSendResult = { + status: PaymentSendStatus.Failure, + transaction: { walletId } as WalletTransaction, + } + + beforeEach(() => { + jest.clearAllMocks() + mockApiKeys.checkAndLockSpending.mockResolvedValue("ephemeral-id" as EphemeralId) + mockApiKeys.recordSpending.mockResolvedValue(true) + mockApiKeys.reverseSpending.mockResolvedValue(true) + mockCheckIntraledgerLimits.mockResolvedValue(true) + mockCheckTradeIntraAccountLimits.mockResolvedValue(true) + mockCheckWithdrawalLimits.mockResolvedValue(true) + }) + + it("returns account limit error without attempting api key lock", async () => { + const accountLimitError = new ApiKeyLimitExceededError() + mockCheckWithdrawalLimits.mockResolvedValue(accountLimitError) + + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toBe(accountLimitError) + expect(mockApiKeys.checkAndLockSpending).not.toHaveBeenCalled() + }) + + it("records settlement when execution succeeds with settlement transaction id", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockApiKeys.checkAndLockSpending).toHaveBeenCalledWith({ + apiKeyId, + amount: btcPaymentAmount, + }) + expect(mockApiKeys.recordSpending).toHaveBeenCalled() + expect(mockApiKeys.reverseSpending).not.toHaveBeenCalled() + }) + + it("reverses settlement when execution returns reverse intent for already-paid result", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => reverseSettlement({ result: paymentSendAlreadyPaidResult }), + }) + + expect(result).toEqual(paymentSendAlreadyPaidResult) + expect(mockApiKeys.reverseSpending).toHaveBeenCalled() + expect(mockApiKeys.recordSpending).not.toHaveBeenCalled() + }) + + it("reverses settlement when execution returns reverse intent for failure result", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => reverseSettlement({ result: paymentSendFailureResult }), + }) + + expect(result).toEqual(paymentSendFailureResult) + expect(mockApiKeys.reverseSpending).toHaveBeenCalled() + expect(mockApiKeys.recordSpending).not.toHaveBeenCalled() + }) + + it("reverses settlement when execution error is returned with reverse intent", async () => { + const executionError = new ApiKeyLimitExceededError() + + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => reverseSettlement({ result: executionError }), + }) + + expect(result).toBe(executionError) + expect(mockApiKeys.reverseSpending).toHaveBeenCalled() + expect(mockApiKeys.recordSpending).not.toHaveBeenCalled() + }) + + it("records settlement for lightning-style post-journal lookup errors", async () => { + const executionError = new CouldNotFindError("wallet transaction") + + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: executionError, + settlementTransactionId: journalId, + }), + }) + + expect(result).toBe(executionError) + expect(mockApiKeys.recordSpending).toHaveBeenCalled() + expect(mockApiKeys.reverseSpending).not.toHaveBeenCalled() + }) + + it("records settlement for on-chain-style post-journal lookup errors", async () => { + const executionError = new CouldNotFindError("wallet transaction") + + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.OnChain, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: executionError, + settlementTransactionId: journalId, + }), + }) + + expect(result).toBe(executionError) + expect(mockApiKeys.recordSpending).toHaveBeenCalled() + expect(mockApiKeys.reverseSpending).not.toHaveBeenCalled() + }) + + it("records exception when settlement fails", async () => { + const settlementError = new ApiKeyLimitExceededError() + mockApiKeys.recordSpending.mockResolvedValue(settlementError) + + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockRecordExceptionInCurrentSpan).toHaveBeenCalledWith({ + error: settlementError, + }) + }) + + it("checks trade intra-account limits for intraledger self transfer", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.IntraLedger, + accountId: "same-account-id" as AccountId, + recipientAccountId: "same-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockCheckTradeIntraAccountLimits).toHaveBeenCalledTimes(1) + expect(mockCheckIntraledgerLimits).not.toHaveBeenCalled() + expect(mockCheckWithdrawalLimits).not.toHaveBeenCalled() + }) + + it("checks intraledger limits for intraledger transfer to other account", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.IntraLedger, + accountId: "sender-account-id" as AccountId, + recipientAccountId: "recipient-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockCheckIntraledgerLimits).toHaveBeenCalledTimes(1) + expect(mockCheckTradeIntraAccountLimits).not.toHaveBeenCalled() + }) + + it("checks withdrawal limits for non-intraledger settlement method", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.OnChain, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + recordSettlement({ + result: paymentSendSuccessResult, + settlementTransactionId: journalId, + }), + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockCheckWithdrawalLimits).toHaveBeenCalledTimes(1) + }) + + it("reverses and records error-level trace on invalid record settlement payload", async () => { + const result = await withSpendingLimits({ + settlementMethod: SettlementMethod.Lightning, + accountId: "sender-account-id" as AccountId, + usdPaymentAmount: { amount: 1000n, currency: "USD" } as UsdPaymentAmount, + priceRatioForLimits: {} as WalletPriceRatio, + apiKeyId, + btcPaymentAmount, + execute: async () => + ({ + apiKeySettlement: ApiKeySpendingSettlementType.Record, + result: paymentSendSuccessResult, + }) as SpendingLimitsExecutionResult, + }) + + expect(result).toEqual(paymentSendSuccessResult) + expect(mockApiKeys.reverseSpending).toHaveBeenCalled() + expect(mockApiKeys.recordSpending).not.toHaveBeenCalled() + expect(mockRecordExceptionInCurrentSpan).toHaveBeenCalledWith( + expect.objectContaining({ level: ErrorLevel.Critical }), + ) + }) +}) diff --git a/core/api/test/unit/servers/event-handlers/bria.spec.ts b/core/api/test/unit/servers/event-handlers/bria.spec.ts new file mode 100644 index 0000000000..09856e764a --- /dev/null +++ b/core/api/test/unit/servers/event-handlers/bria.spec.ts @@ -0,0 +1,103 @@ +const mockRecordOnChainSendRevert = jest.fn() +const mockReverseSpending = jest.fn() +const mockRecordExceptionInCurrentSpan = jest.fn() + +jest.mock("@/app", () => ({ + Wallets: {}, + OnChain: {}, +})) + +jest.mock("@/services/ledger/facade", () => ({ + recordOnChainSendRevert: (...args: unknown[]) => mockRecordOnChainSendRevert(...args), +})) + +jest.mock("@/services/api-keys", () => ({ + ApiKeysService: () => ({ + reverseSpending: (...args: unknown[]) => mockReverseSpending(...args), + }), +})) + +jest.mock("@/services/bria", () => ({ + BriaPayloadType: { + UtxoDetected: "utxo_detected", + UtxoDropped: "utxo_dropped", + UtxoSettled: "utxo_settled", + PayoutSubmitted: "payout_submitted", + PayoutCommitted: "payout_committed", + PayoutCancelled: "payout_cancelled", + PayoutBroadcast: "payout_broadcast", + PayoutSettled: "payout_settled", + }, +})) + +jest.mock("@/services/tracing", () => ({ + recordExceptionInCurrentSpan: (...args: unknown[]) => + mockRecordExceptionInCurrentSpan(...args), + addAttributesToCurrentSpan: jest.fn(), +})) + +import { payoutCancelledEventHandler } from "@/servers/event-handlers/bria" + +import { NoTransactionToUpdateError } from "@/domain/errors" +import { ApiKeySpendingRecordError } from "@/domain/api-keys/errors" + +describe("payoutCancelledEventHandler", () => { + const payoutInfo = { + id: "payout-id", + externalId: "journal-id", + } as PayoutAugmentation + + const event = { + type: "payout_cancelled", + id: "payout-id", + satoshis: { amount: 1000n, currency: "BTC" }, + address: "bcrt1qtestaddress", + } as PayoutCancelled + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("reverses api key spending after payout cancellation", async () => { + mockRecordOnChainSendRevert.mockResolvedValue(true) + mockReverseSpending.mockResolvedValue(true) + + const result = await payoutCancelledEventHandler({ event, payoutInfo }) + + expect(result).toBe(true) + expect(mockRecordOnChainSendRevert).toHaveBeenCalledWith({ + journalId: payoutInfo.externalId, + payoutId: event.id, + }) + expect(mockReverseSpending).toHaveBeenCalledWith({ + transactionId: payoutInfo.externalId, + }) + }) + + it("records trace exception when api key reversal fails", async () => { + const reverseError = new ApiKeySpendingRecordError() + + mockRecordOnChainSendRevert.mockResolvedValue(true) + mockReverseSpending.mockResolvedValue(reverseError) + + const result = await payoutCancelledEventHandler({ event, payoutInfo }) + + expect(result).toBe(true) + expect(mockRecordExceptionInCurrentSpan).toHaveBeenCalledWith({ + error: reverseError, + attributes: { + "apiKeys.reverseSpending.failed": true, + "journalId": payoutInfo.externalId, + }, + }) + }) + + it("keeps idempotent behavior when ledger transaction is not found", async () => { + mockRecordOnChainSendRevert.mockResolvedValue(new NoTransactionToUpdateError()) + + const result = await payoutCancelledEventHandler({ event, payoutInfo }) + + expect(result).toBe(true) + expect(mockReverseSpending).not.toHaveBeenCalled() + }) +}) diff --git a/lib/gt3-server-node-express-sdk/package.json b/lib/gt3-server-node-express-sdk/package.json index 4fd517d214..59e12b0d61 100644 --- a/lib/gt3-server-node-express-sdk/package.json +++ b/lib/gt3-server-node-express-sdk/package.json @@ -8,7 +8,7 @@ "preinstall": "npx only-allow pnpm" }, "dependencies": { - "axios": "^1.11.0", + "axios": "^1.15.0", "qs": "^6.11.2", "string-random": "^0.1.3" } diff --git a/package.json b/package.json index 5709a4ef8d..1d79790f80 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "protobufjs": "7.2.5", "http-cache-semantics": "4.1.1", "elliptic": "^6.6.1", - "form-data": "^4.0.4" + "form-data": "^4.0.4", + "handlebars": "^4.7.9" }, "packageManager": "pnpm@8.7.6" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f1699b698..43306628b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: http-cache-semantics: 4.1.1 elliptic: ^6.6.1 form-data: ^4.0.4 + handlebars: ^4.7.9 importers: @@ -197,8 +198,8 @@ importers: specifier: ^0.7.1 version: 0.7.3(typescript@5.3.3)(zod@3.25.67) axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.15.0 + version: 1.15.0 dotenv: specifier: ^16.3.1 version: 16.6.1 @@ -1174,11 +1175,11 @@ importers: specifier: ^8.17.1 version: 8.17.1 axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.15.0 + version: 1.15.0 axios-retry: specifier: ^4.5.0 - version: 4.5.0(axios@1.11.0) + version: 4.5.0(axios@1.15.0) basic-auth: specifier: ^2.0.1 version: 2.0.1 @@ -1479,7 +1480,7 @@ importers: version: 0.1.4 axios-mock-adapter: specifier: ^2.1.0 - version: 2.1.0(axios@1.11.0) + version: 2.1.0(axios@1.15.0) eslint: specifier: ^9.30.1 version: 9.30.1 @@ -1713,8 +1714,8 @@ importers: lib/gt3-server-node-express-sdk: dependencies: axios: - specifier: ^1.11.0 - version: 1.11.0 + specifier: ^1.15.0 + version: 1.15.0 qs: specifier: ^6.11.2 version: 6.14.0 @@ -2910,7 +2911,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 1.9.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -2933,7 +2934,7 @@ packages: '@babel/traverse': 7.27.7 '@babel/types': 7.27.7 convert-source-map: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4676,7 +4677,7 @@ packages: combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 4.0.4 + form-data: 4.0.5 http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 @@ -5455,7 +5456,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -5471,7 +5472,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -7154,7 +7155,7 @@ packages: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.8 chalk: 4.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dotenv: 16.6.1 graphql: 16.11.0 graphql-request: 6.1.0(graphql@16.11.0) @@ -7453,7 +7454,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8328,7 +8329,7 @@ packages: dependencies: '@types/node': 22.16.0 async-exit-hook: 2.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -8907,7 +8908,7 @@ packages: /@ory/client@1.20.22: resolution: {integrity: sha512-N0UixgrJ7Xi0O5bttaFbJrtQhZAi61z69d9p/3tn71/nfBPOVTb8mOJIOlPhQIECj/11A65ttzI2R0QhEIeQEw==} dependencies: - axios: 1.11.0 + axios: 1.15.0 transitivePeerDependencies: - debug dev: false @@ -8915,7 +8916,7 @@ packages: /@ory/hydra-client@2.2.1: resolution: {integrity: sha512-Hb6GQuRwPyxt44Cvd3AMFKw/UtvGCHeXO7kIDfv95C/8kErWukCA3WBDWeVfZuIjY+HVPeZFZkBRVC+Ub8UEVg==} dependencies: - axios: 1.11.0 + axios: 1.15.0 transitivePeerDependencies: - debug dev: false @@ -11997,7 +11998,7 @@ packages: find-up: 5.0.0 fs-extra: 11.3.0 glob: 10.4.5 - handlebars: 4.7.8 + handlebars: 4.7.9 lazy-universal-dotenv: 4.0.0 node-fetch: 2.7.0 picomatch: 2.3.1 @@ -13584,7 +13585,7 @@ packages: resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} dependencies: '@types/node': 20.11.24 - form-data: 4.0.4 + form-data: 4.0.5 /@types/node-jose@1.1.13: resolution: {integrity: sha512-QjMd4yhwy1EvSToQn0YI3cD29YhyfxFwj7NecuymjLys2/P0FwxWnkgBlFxCai6Y3aBCe7rbwmqwJJawxlgcXw==} @@ -13956,7 +13957,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -13984,7 +13985,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -14100,7 +14101,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14120,7 +14121,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -14141,7 +14142,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -14162,7 +14163,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.3.3 transitivePeerDependencies: @@ -14183,7 +14184,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -14204,7 +14205,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14225,7 +14226,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.2.2 transitivePeerDependencies: @@ -14246,7 +14247,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.6.3 transitivePeerDependencies: @@ -14267,7 +14268,7 @@ packages: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 typescript: 5.8.3 transitivePeerDependencies: @@ -14285,7 +14286,7 @@ packages: '@typescript-eslint/types': 8.35.1 '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.35.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: @@ -14404,7 +14405,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 tsutils: 3.21.0(typescript@5.6.3) typescript: 5.6.3 @@ -14424,7 +14425,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.2.2) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.2.2) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.2.2) typescript: 5.2.2 @@ -14444,7 +14445,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.8.3) - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.8.3) typescript: 5.8.3 @@ -14676,7 +14677,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14698,7 +14699,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14720,7 +14721,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -16095,43 +16096,33 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - /axios-mock-adapter@2.1.0(axios@1.11.0): + /axios-mock-adapter@2.1.0(axios@1.15.0): resolution: {integrity: sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==} peerDependencies: axios: '>= 0.17.0' dependencies: - axios: 1.11.0 + axios: 1.15.0 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 dev: true - /axios-retry@4.5.0(axios@1.11.0): + /axios-retry@4.5.0(axios@1.15.0): resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} peerDependencies: axios: 0.x || 1.x dependencies: - axios: 1.11.0 + axios: 1.15.0 is-retry-allowed: 2.2.0 dev: false - /axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.4 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - /axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + /axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 - proxy-from-env: 1.1.0 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug - dev: true /axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -17179,7 +17170,7 @@ packages: requiresBuild: true dependencies: '@testim/chrome-version': 1.1.4 - axios: 1.13.5 + axios: 1.15.0 compare-versions: 6.1.1 extract-zip: 2.0.1(supports-color@8.1.1) proxy-agent: 6.5.0 @@ -18208,7 +18199,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@4.4.1(supports-color@8.1.1): resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} @@ -18221,6 +18211,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 8.1.1 + dev: true /debug@4.4.3(supports-color@8.1.1): resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -19445,7 +19436,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.40.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.40.0) get-tsconfig: 4.10.1 @@ -19471,7 +19462,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) eslint: 8.57.0 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.0) get-tsconfig: 4.10.1 @@ -20184,7 +20175,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -20235,7 +20226,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -20292,7 +20283,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -20990,16 +20981,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: true - - /follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true /for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} @@ -21098,6 +21079,7 @@ packages: es-set-tostringtag: 2.1.0 hasown: 2.0.2 mime-types: 2.1.35 + dev: true /form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} @@ -21976,7 +21958,7 @@ packages: hasBin: true dependencies: google-protobuf: 3.15.8 - handlebars: 4.7.7 + handlebars: 4.7.9 dev: true /grunt-cli@1.4.3: @@ -22176,21 +22158,8 @@ packages: duplexer: 0.1.2 dev: true - /handlebars@4.7.7: - resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} - engines: {node: '>=0.4.7'} - hasBin: true - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - dev: true - - /handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + /handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true dependencies: @@ -22216,7 +22185,6 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -22477,7 +22445,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.9 + follow-redirects: 1.15.11 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -22791,7 +22759,7 @@ packages: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -24238,7 +24206,7 @@ packages: dependencies: '@types/express': 4.17.23 '@types/jsonwebtoken': 9.0.10 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) jose: 4.15.9 limiter: 1.1.5 lru-memoizer: 2.3.0 @@ -24519,7 +24487,7 @@ packages: '@aws-crypto/sha256-js': 5.2.0 aes-js: 3.1.2 assert: 2.1.0 - axios: 1.11.0 + axios: 1.15.0 base64-js: 1.5.1 bech32: 2.0.0 bolt11: 1.4.1 @@ -24808,7 +24776,7 @@ packages: chalk: 4.1.2 commander: 7.2.0 commondir: 1.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1(supports-color@5.5.0) dependency-tree: 9.0.0 detective-amd: 4.2.0 detective-cjs: 4.1.0 @@ -27239,11 +27207,16 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: true /proxy-from-env@2.0.0: resolution: {integrity: sha512-h2lD3OfRraP3R51rNFKIE8nX+qoLr1mE74X91YhVxtDbt+OD6ntoNZv56+JgI4RCdtwQ5eexsOk1KdOQDfvPCQ==} dev: true + /proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + /prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} requiresBuild: true @@ -29045,7 +29018,7 @@ packages: grunt-contrib-uglify: 5.2.2 grunt-contrib-watch: 1.1.0 grunt-sass: 3.1.0(grunt@1.5.3) - handlebars: 4.7.8 + handlebars: 4.7.9 highlight.js: 11.11.1 htmlparser2: 9.0.0 js-beautify: 1.14.11 @@ -29539,7 +29512,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -30594,7 +30566,7 @@ packages: resolution: {integrity: sha512-aJLBvI7ODLmFHI7ZYLBiMZKIdHuF9PrPeRM/GBMDg/AAzGXs4V8gEnNPHyTVThK0/8J48YHSqXMlQ+WJR5nxoQ==} engines: {node: '>=14.0'} dependencies: - axios: 1.11.0 + axios: 1.15.0 dayjs: 1.11.13 https-proxy-agent: 5.0.1 jsonwebtoken: 9.0.2