diff --git a/apps/dashboard/app/api-keys/server-actions.ts b/apps/dashboard/app/api-keys/server-actions.ts index f27072ceea..fb502d30b6 100644 --- a/apps/dashboard/app/api-keys/server-actions.ts +++ b/apps/dashboard/app/api-keys/server-actions.ts @@ -5,8 +5,13 @@ import { revalidatePath } from "next/cache" import { ApiKeyResponse } from "./api-key.types" import { authOptions } from "@/app/api/auth/[...nextauth]/route" -import { createApiKey, revokeApiKey } from "@/services/graphql/mutations/api-keys" -import { Scope } from "@/services/graphql/generated" +import { + createApiKey, + revokeApiKey, + setApiKeyLimit, + removeApiKeyLimit, +} from "@/services/graphql/mutations/api-keys" +import { LimitTimeWindow, Scope } from "@/services/graphql/generated" export const revokeApiKeyServerAction = async (id: string) => { if (!id || typeof id !== "string") { @@ -113,9 +118,263 @@ export const createApiKeyServerAction = async ( } } + if (data?.apiKeyCreate.apiKey.id) { + const apiKeyId = data.apiKeyCreate.apiKey.id + try { + const limitFields: Array<{ formField: string; timeWindow: LimitTimeWindow }> = [ + { formField: "dailyLimitSats", timeWindow: LimitTimeWindow.Daily }, + { formField: "weeklyLimitSats", timeWindow: LimitTimeWindow.Weekly }, + { formField: "monthlyLimitSats", timeWindow: LimitTimeWindow.Monthly }, + { formField: "annualLimitSats", timeWindow: LimitTimeWindow.Annual }, + ] + + for (const { formField, timeWindow } of limitFields) { + const value = form.get(formField) + if (value && value !== "") { + const limit = parseInt(value as string, 10) + if (limit > 0) { + await setApiKeyLimit({ + id: apiKeyId, + limitTimeWindow: timeWindow, + limitSats: limit, + }) + } + } + } + } catch (err) { + console.log("error in setting API key limits ", err) + // Don't fail the entire operation if limits fail to set + // The API key was created successfully + } + } + return { error: false, message: "API Key created successfully", responsePayload: { apiKeySecret: data?.apiKeyCreate.apiKeySecret }, } } + +export const setDailyLimit = async ({ + id, + dailyLimitSats, +}: { + id: string + dailyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!dailyLimitSats || dailyLimitSats <= 0) { + throw new Error("Daily limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Daily, + limitSats: dailyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (daily) ", err) + throw new Error("Failed to set API key daily limit") + } + + revalidatePath("/api-keys") +} + +export const setWeeklyLimit = async ({ + id, + weeklyLimitSats, +}: { + id: string + weeklyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!weeklyLimitSats || weeklyLimitSats <= 0) { + throw new Error("Weekly limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Weekly, + limitSats: weeklyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (weekly) ", err) + throw new Error("Failed to set API key weekly limit") + } + + revalidatePath("/api-keys") +} + +export const setMonthlyLimit = async ({ + id, + monthlyLimitSats, +}: { + id: string + monthlyLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!monthlyLimitSats || monthlyLimitSats <= 0) { + throw new Error("Monthly limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Monthly, + limitSats: monthlyLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (monthly) ", err) + throw new Error("Failed to set API key monthly limit") + } + + revalidatePath("/api-keys") +} + +export const setAnnualLimit = async ({ + id, + annualLimitSats, +}: { + id: string + annualLimitSats: number +}) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + if (!annualLimitSats || annualLimitSats <= 0) { + throw new Error("Annual limit must be greater than 0") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await setApiKeyLimit({ + id, + limitTimeWindow: LimitTimeWindow.Annual, + limitSats: annualLimitSats, + }) + } catch (err) { + console.log("error in setApiKeyLimit (annual) ", err) + throw new Error("Failed to set API key annual limit") + } + + revalidatePath("/api-keys") +} + +export const removeLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Daily }) + } catch (err) { + console.log("error in removeApiKeyLimit (daily) ", err) + throw new Error("Failed to remove API key limit") + } + + revalidatePath("/api-keys") +} + +export const removeWeeklyLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Weekly }) + } catch (err) { + console.log("error in removeApiKeyLimit (weekly) ", err) + throw new Error("Failed to remove API key weekly limit") + } + + revalidatePath("/api-keys") +} + +export const removeMonthlyLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Monthly }) + } catch (err) { + console.log("error in removeApiKeyLimit (monthly) ", err) + throw new Error("Failed to remove API key monthly limit") + } + + revalidatePath("/api-keys") +} + +export const removeAnnualLimit = async ({ id }: { id: string }) => { + if (!id || typeof id !== "string") { + throw new Error("API Key ID is not present") + } + + const session = await getServerSession(authOptions) + const token = session?.accessToken + if (!token || typeof token !== "string") { + throw new Error("Token is not present") + } + + try { + await removeApiKeyLimit({ id, limitTimeWindow: LimitTimeWindow.Annual }) + } catch (err) { + console.log("error in removeApiKeyLimit (annual) ", err) + throw new Error("Failed to remove API key annual limit") + } + + revalidatePath("/api-keys") +} diff --git a/apps/dashboard/components/api-keys/form.tsx b/apps/dashboard/components/api-keys/form.tsx index 9d71c925f4..ecdec3f881 100644 --- a/apps/dashboard/components/api-keys/form.tsx +++ b/apps/dashboard/components/api-keys/form.tsx @@ -23,6 +23,7 @@ type ApiKeyFormProps = { const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => { const [enableCustomExpiresInDays, setEnableCustomExpiresInDays] = useState(false) const [expiresInDays, setExpiresInDays] = useState(null) + const [showSpendingLimits, setShowSpendingLimits] = useState(false) const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() @@ -53,6 +54,11 @@ const ApiKeyForm = ({ state, formAction }: ApiKeyFormProps) => { )} {state.error && } + + {showSpendingLimits && } @@ -179,6 +185,84 @@ const ScopeCheckboxes = () => ( ) +const SpendingLimitsToggle = ({ + showSpendingLimits, + setShowSpendingLimits, +}: { + showSpendingLimits: boolean + setShowSpendingLimits: (value: boolean) => void +}) => ( + + setShowSpendingLimits(e.target.checked)} + label="Set budget limits" + /> + +) + +const SpendingLimitsInputs = () => ( + + + Limits (in satoshis) + + + Daily Limit + + Rolling 24-hour window + + + Weekly Limit + + Rolling 7-day window + + + Monthly Limit + + Rolling 30-day window + + + Annual Limit + + Rolling 365-day window + + +) + const SubmitButton = () => ( = ({ id, limits, spent }) => { + const [open, setOpen] = useState(false) + const [selectedPeriod, setSelectedPeriod] = useState("daily") + const [limitValues, setLimitValues] = useState({ + daily: limits.daily?.toString() || "", + weekly: limits.weekly?.toString() || "", + monthly: limits.monthly?.toString() || "", + annual: limits.annual?.toString() || "", + }) + const [loading, setLoading] = useState(false) + + const periodConfig = { + daily: { + label: "Daily (24h)", + description: "Set a rolling 24-hour spending limit", + currentLimit: limits.daily, + spent: spent.last24h, + setValue: (val: string) => setLimitValues({ ...limitValues, daily: val }), + getValue: () => limitValues.daily, + }, + weekly: { + label: "Weekly (7 days)", + description: "Set a rolling 7-day spending limit", + currentLimit: limits.weekly, + spent: spent.last7d, + setValue: (val: string) => setLimitValues({ ...limitValues, weekly: val }), + getValue: () => limitValues.weekly, + }, + monthly: { + label: "Monthly (30 days)", + description: "Set a rolling 30-day spending limit", + currentLimit: limits.monthly, + spent: spent.last30d, + setValue: (val: string) => setLimitValues({ ...limitValues, monthly: val }), + getValue: () => limitValues.monthly, + }, + annual: { + label: "Annual (365 days)", + description: "Set a rolling 365-day spending limit", + currentLimit: limits.annual, + spent: spent.last365d, + setValue: (val: string) => setLimitValues({ ...limitValues, annual: val }), + getValue: () => limitValues.annual, + }, + } + + const handleSetLimit = async (period: LimitPeriod) => { + const config = periodConfig[period] + const limitValue = config.getValue() + + if (!limitValue || parseInt(limitValue) <= 0) { + alert("Please enter a valid limit in satoshis") + return + } + + setLoading(true) + try { + const satsValue = parseInt(limitValue) + switch (period) { + case "daily": + await setDailyLimit({ id, dailyLimitSats: satsValue }) + break + case "weekly": + await setWeeklyLimit({ id, weeklyLimitSats: satsValue }) + break + case "monthly": + await setMonthlyLimit({ id, monthlyLimitSats: satsValue }) + break + case "annual": + await setAnnualLimit({ id, annualLimitSats: satsValue }) + break + } + setOpen(false) + window.location.reload() + } catch (error) { + console.error("Error setting limit:", error) + alert("Failed to set limit. Please try again.") + } finally { + setLoading(false) + } + } + + const handleRemoveLimit = async (period: LimitPeriod) => { + const periodLabels = { + daily: "daily", + weekly: "weekly", + monthly: "monthly", + annual: "annual", + } + + if ( + !confirm( + `Are you sure you want to remove the ${periodLabels[period]} spending limit?`, + ) + ) { + return + } + + setLoading(true) + try { + switch (period) { + case "daily": + await removeLimit({ id }) + break + case "weekly": + await removeWeeklyLimit({ id }) + break + case "monthly": + await removeMonthlyLimit({ id }) + break + case "annual": + await removeAnnualLimit({ id }) + break + } + setOpen(false) + window.location.reload() + } catch (error) { + console.error("Error removing limit:", error) + alert("Failed to remove limit. Please try again.") + } finally { + setLoading(false) + } + } + + const formatSats = (sats: number | null) => { + if (sats === null) return "Unlimited" + return `${sats.toLocaleString()} sats` + } + + const hasAnyLimit = limits.daily || limits.weekly || limits.monthly || limits.annual + + return ( + <> + + + setOpen(false)}> + + Budget Limits + + Configure rolling budget limits for different time periods + + + setSelectedPeriod(value as LimitPeriod)} + > + + Daily + Weekly + Monthly + Annual + + + {(Object.keys(periodConfig) as LimitPeriod[]).map((period) => { + const config = periodConfig[period] + const remaining = config.currentLimit + ? config.currentLimit - config.spent + : null + + return ( + + + {config.description} + + {config.currentLimit && ( + + + + Current Limit:{" "} + {formatSats(config.currentLimit)} + + + Spent: {formatSats(config.spent)} + + + Remaining: {formatSats(remaining)} + + + + )} + + + {config.label} Limit (satoshis) + config.setValue(e.target.value)} + placeholder="Enter limit in sats (e.g., 100000)" + disabled={loading} + /> + + + + + {config.currentLimit && ( + + )} + + + + ) + })} + + + + + + + + + ) +} + +export default Limit diff --git a/apps/dashboard/components/api-keys/list.tsx b/apps/dashboard/components/api-keys/list.tsx index 13d8a9d217..5e94cd7ff4 100644 --- a/apps/dashboard/components/api-keys/list.tsx +++ b/apps/dashboard/components/api-keys/list.tsx @@ -1,9 +1,13 @@ +"use client" + import React from "react" import Table from "@mui/joy/Table" import Typography from "@mui/joy/Typography" import Divider from "@mui/joy/Divider" +import { Stack } from "@mui/joy" import RevokeKey from "./revoke" +import Limit from "./limit" import { formatDate, getScopeText } from "./utils" import { ApiKey } from "@/services/graphql/generated" @@ -25,25 +29,117 @@ const ApiKeysList: React.FC = ({ - - - - - - + + + + + + + - {activeKeys.map(({ id, name, expiresAt, lastUsedAt, scopes }) => { + {activeKeys.map(({ id, name, expiresAt, lastUsedAt, scopes, limits }) => { + const dailyLimitSats = limits?.dailyLimitSats + const weeklyLimitSats = limits?.weeklyLimitSats + const monthlyLimitSats = limits?.monthlyLimitSats + const annualLimitSats = limits?.annualLimitSats + const dailySpentSats = limits?.dailySpentSats ?? 0 + const weeklySpentSats = limits?.weeklySpentSats ?? 0 + const monthlySpentSats = limits?.monthlySpentSats ?? 0 + const annualSpentSats = limits?.annualSpentSats ?? 0 + + const remainingDailyLimitSats = + dailyLimitSats !== null && dailyLimitSats !== undefined + ? dailyLimitSats - dailySpentSats + : null + + const hasAnyLimit = + dailyLimitSats || weeklyLimitSats || monthlyLimitSats || annualLimitSats + return ( + ) diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index b4535f35c9..86451398d9 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -2536,7 +2536,7 @@ export type ApiKeyCreateMutationVariables = Exact<{ }>; -export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray } } }; +export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; export type ApiKeyRevokeMutationVariables = Exact<{ input: ApiKeyRevokeInput; @@ -2545,6 +2545,20 @@ export type ApiKeyRevokeMutationVariables = Exact<{ export type ApiKeyRevokeMutation = { readonly __typename: 'Mutation', readonly apiKeyRevoke: { readonly __typename: 'ApiKeyRevokePayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray } } }; +export type ApiKeySetLimitMutationVariables = Exact<{ + input: ApiKeySetLimitInput; +}>; + + +export type ApiKeySetLimitMutation = { readonly __typename: 'Mutation', readonly apiKeySetLimit: { readonly __typename: 'ApiKeySetLimitPayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; + +export type ApiKeyRemoveLimitMutationVariables = Exact<{ + input: ApiKeyRemoveLimitInput; +}>; + + +export type ApiKeyRemoveLimitMutation = { readonly __typename: 'Mutation', readonly apiKeyRemoveLimit: { readonly __typename: 'ApiKeySetLimitPayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly limits: { readonly __typename: 'ApiKeyLimits', readonly dailyLimitSats?: number | null, readonly weeklyLimitSats?: number | null, readonly monthlyLimitSats?: number | null, readonly annualLimitSats?: number | null, readonly dailySpentSats: number, readonly weeklySpentSats: number, readonly monthlySpentSats: number, readonly annualSpentSats: number } } } }; + export type CallbackEndpointAddMutationVariables = Exact<{ input: CallbackEndpointAddInput; }>; @@ -2667,6 +2681,16 @@ export const ApiKeyCreateDocument = gql` lastUsedAt expiresAt scopes + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } } apiKeySecret } @@ -2740,6 +2764,98 @@ export function useApiKeyRevokeMutation(baseOptions?: Apollo.MutationHookOptions export type ApiKeyRevokeMutationHookResult = ReturnType; export type ApiKeyRevokeMutationResult = Apollo.MutationResult; export type ApiKeyRevokeMutationOptions = Apollo.BaseMutationOptions; +export const ApiKeySetLimitDocument = gql` + mutation ApiKeySetLimit($input: ApiKeySetLimitInput!) { + apiKeySetLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } +} + `; +export type ApiKeySetLimitMutationFn = Apollo.MutationFunction; + +/** + * __useApiKeySetLimitMutation__ + * + * To run a mutation, you first call `useApiKeySetLimitMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useApiKeySetLimitMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [apiKeySetLimitMutation, { data, loading, error }] = useApiKeySetLimitMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useApiKeySetLimitMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ApiKeySetLimitDocument, options); + } +export type ApiKeySetLimitMutationHookResult = ReturnType; +export type ApiKeySetLimitMutationResult = Apollo.MutationResult; +export type ApiKeySetLimitMutationOptions = Apollo.BaseMutationOptions; +export const ApiKeyRemoveLimitDocument = gql` + mutation ApiKeyRemoveLimit($input: ApiKeyRemoveLimitInput!) { + apiKeyRemoveLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } +} + `; +export type ApiKeyRemoveLimitMutationFn = Apollo.MutationFunction; + +/** + * __useApiKeyRemoveLimitMutation__ + * + * To run a mutation, you first call `useApiKeyRemoveLimitMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useApiKeyRemoveLimitMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [apiKeyRemoveLimitMutation, { data, loading, error }] = useApiKeyRemoveLimitMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useApiKeyRemoveLimitMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ApiKeyRemoveLimitDocument, options); + } +export type ApiKeyRemoveLimitMutationHookResult = ReturnType; +export type ApiKeyRemoveLimitMutationResult = Apollo.MutationResult; +export type ApiKeyRemoveLimitMutationOptions = Apollo.BaseMutationOptions; export const CallbackEndpointAddDocument = gql` mutation CallbackEndpointAdd($input: CallbackEndpointAddInput!) { callbackEndpointAdd(input: $input) { diff --git a/apps/dashboard/services/graphql/mutations/api-keys.ts b/apps/dashboard/services/graphql/mutations/api-keys.ts index a0a0a414d8..6326c85b6e 100644 --- a/apps/dashboard/services/graphql/mutations/api-keys.ts +++ b/apps/dashboard/services/graphql/mutations/api-keys.ts @@ -6,6 +6,11 @@ import { ApiKeyCreateMutation, ApiKeyRevokeDocument, ApiKeyRevokeMutation, + ApiKeySetLimitDocument, + ApiKeySetLimitMutation, + ApiKeyRemoveLimitDocument, + ApiKeyRemoveLimitMutation, + LimitTimeWindow, Scope, } from "../generated" @@ -21,6 +26,16 @@ gql` lastUsedAt expiresAt scopes + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } } apiKeySecret } @@ -40,6 +55,44 @@ gql` } } } + + mutation ApiKeySetLimit($input: ApiKeySetLimitInput!) { + apiKeySetLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } + } + + mutation ApiKeyRemoveLimit($input: ApiKeyRemoveLimitInput!) { + apiKeyRemoveLimit(input: $input) { + apiKey { + id + name + limits { + dailyLimitSats + weeklyLimitSats + monthlyLimitSats + annualLimitSats + dailySpentSats + weeklySpentSats + monthlySpentSats + annualSpentSats + } + } + } + } ` export async function createApiKey({ @@ -77,3 +130,45 @@ export async function revokeApiKey({ id }: { id: string }) { throw new Error("Error in apiKeyRevoke") } } + +export async function setApiKeyLimit({ + id, + limitTimeWindow, + limitSats, +}: { + id: string + limitTimeWindow: LimitTimeWindow + limitSats: number +}) { + const client = await apolloClient.authenticated() + try { + const { data } = await client.mutate({ + mutation: ApiKeySetLimitDocument, + variables: { input: { id, limitTimeWindow, limitSats } }, + }) + return data + } catch (error) { + console.error("Error executing mutation: apiKeySetLimit ==> ", error) + throw new Error("Error in apiKeySetLimit") + } +} + +export async function removeApiKeyLimit({ + id, + limitTimeWindow, +}: { + id: string + limitTimeWindow: LimitTimeWindow +}) { + const client = await apolloClient.authenticated() + try { + const { data } = await client.mutate({ + mutation: ApiKeyRemoveLimitDocument, + variables: { input: { id, limitTimeWindow } }, + }) + return data + } catch (error) { + console.error("Error executing mutation: apiKeyRemoveLimit ==> ", error) + throw new Error("Error in apiKeyRemoveLimit") + } +} 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..9255a25c55 --- /dev/null +++ b/bats/core/api-keys/api-keys-hold-invoice-reversal.bats @@ -0,0 +1,148 @@ +#!/usr/bin/env bats + +# Tests for API key spending reversal when hold invoices are canceled/timeout + +load "../../helpers/_common.bash" +load "../../helpers/cli.bash" +load "../../helpers/user.bash" +load "../../helpers/onchain.bash" +load "../../helpers/ln.bash" + +random_uuid() { + if [[ -e /proc/sys/kernel/random/uuid ]]; then + cat /proc/sys/kernel/random/uuid + else + uuidgen + fi +} + +new_key_name() { + random_uuid +} + +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' +} + +@test "hold-invoice-reversal: create api key with daily limit" { + key_name="$(new_key_name)" + 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" + + # Set daily limit to 10000 sats + 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" { + # Generate a random preimage and hash for the hold invoice + 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" + + # Create a hold invoice on external LND with explicit 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" + + # Pay the invoice with API key + 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')" + + # Payment should be pending (held) + [[ "${send_status}" = "PENDING" || "${send_status}" = "SUCCESS" ]] || exit 1 + + # Check that spending was recorded + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'hold_invoice_key_name')'") | .limits.dailySpentSats')" + + # Should have recorded 5000 sats spending + [[ "${spent_24h}" -ge "5000" ]] || exit 1 + + echo "✅ Spending recorded: ${spent_24h} sats" +} + +@test "hold-invoice-reversal: verify payment is held on external LND" { + payment_hash="$(read_value 'hold-invoice-payment-hash')" + + # Check invoice status on external LND + invoice_info="$(lnd_outside_cli lookupinvoice "$payment_hash")" + state="$(echo $invoice_info | jq -r '.state')" + + # Invoice should be in ACCEPTED state (held, not settled) + [[ "${state}" = "ACCEPTED" ]] || exit 1 + + echo "✅ Invoice is held (ACCEPTED state)" + echo "Payment hash: $payment_hash" +} + +@test "hold-invoice-reversal: cancel hold invoice" { + payment_hash="$(read_value 'hold-invoice-payment-hash')" + + # Cancel the held invoice on external LND + cancel_result="$(lnd_outside_cli cancelinvoice "$payment_hash" 2>&1 || true)" + + echo "Cancel result: $cancel_result" + + # Give trigger server time to process the cancellation + sleep 10 +} + +@test "hold-invoice-reversal: verify spending was reversed after cancellation" { + # Trigger the pending payment update to process the cancellation + # This simulates what the trigger server does every 5 minutes + + # Wait a bit more for the system to process + sleep 5 + + # Check that spending was reversed + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'hold_invoice_key_name')'") | .limits.dailySpentSats')" + + # Spending should be back to 0 after reversal + [[ "${spent_24h}" = "0" ]] || exit 1 + + echo "✅ Spending reversed: ${spent_24h} sats" +} \ No newline at end of file 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..02f8c70b38 --- /dev/null +++ b/bats/core/api-keys/api-keys-limits.bats @@ -0,0 +1,709 @@ +#!/usr/bin/env bats + +load "../../helpers/_common.bash" +load "../../helpers/cli.bash" +load "../../helpers/user.bash" +load "../../helpers/onchain.bash" +load "../../helpers/ln.bash" + +random_uuid() { + if [[ -e /proc/sys/kernel/random/uuid ]]; then + cat /proc/sys/kernel/random/uuid + else + uuidgen + fi +} + +new_key_name() { + random_uuid +} + +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" + ensure_username_is_present "xyz_zap_receiver" +} + +@test "api-keys-limits: create key and set daily limit" { + key_name="$(new_key_name)" + 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" + + # Set daily limit to 10000 sats + 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 + + # Check spending was recorded + 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')" + + # Should fail due to limit + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + errors="$(graphql_output '.data.intraLedgerPaymentSend.errors | length')" + [[ "${errors}" -ge "1" ]] || exit 1 + + # Verify error message contains limit information + 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") + + # Set weekly limit to 50000 sats + 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 + + # Check total spending across all time periods + exec_graphql 'alice' 'api-keys' + spent_7d="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.weeklySpentSats')" + + # Should have accumulated spending from previous tests + [[ "${spent_7d}" -ge "8000" ]] || exit 1 +} + +@test "api-keys-limits: set monthly and annual limits" { + key_id=$(read_value "limit-api-key-id") + + # Set monthly limit to 100000 sats + 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 + + # Set annual limit to 500000 sats + 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) + + # Try to send 45000 sats - this would exceed weekly limit + 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')" + + # Should fail due to weekly limit + [[ "${send_status}" = "FAILURE" ]] || exit 1 + + # Verify error message contains limit information + 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 + + # Verify spending updated across all time windows + 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')'")')" + + # Verify all limits are still set + 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 + + # Verify spending is consistent across all time windows (since all payments are within last 24h) + 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 + + # Try to send 3000 - should fail as it would exceed updated 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 message contains limit information + 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") + + # Remove weekly limit + 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 + + # Remove monthly limit + 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 + + # Remove annual limit + 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" { + # With all limits removed, should be able to send larger amounts + 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" { + # Create new API key with daily limit for lightning tests + key_name="$(new_key_name)" + 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" + + # Set daily limit to 5000 sats + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":5000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Create invoice for 3000 sats + 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}}' + ) + + # Send lightning payment with API key + exec_graphql 'api-key-ln-secret' 'ln-invoice-payment-send' "$variables" + send_status="$(graphql_output '.data.lnInvoicePaymentSend.status')" + [[ "${send_status}" = "SUCCESS" ]] || exit 1 + + # Verify spending was recorded + 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 message contains limit information + error_msg="$(graphql_output '.data.lnInvoicePaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: onchain payment respects limits" { + # Create new API key with daily limit for onchain tests + key_name="$(new_key_name)" + 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" + + # Set daily limit to 10000 sats + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":10000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Create onchain address + onchain_address=$(bitcoin_cli getnewaddress) + + # Send onchain payment for 5000 sats with API key + 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 + + # Verify spending was recorded + 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 message contains limit information + 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" { + # Verify that each API key tracks its own spending independently + + # Check intraledger key spending (original key from earlier tests) + exec_graphql 'alice' 'api-keys' + intraledger_spent="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'limit_key_name')'") | .limits.dailySpentSats')" + [[ "${intraledger_spent}" -ge "130000" ]] || exit 1 + + # Check lightning key spending (separate key) + 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 + + # Check onchain key spending (separate key) + 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 + + # Each key should have independent spending totals + [[ "${intraledger_spent}" != "${ln_spent}" ]] || exit 1 + [[ "${intraledger_spent}" != "${onchain_spent}" ]] || exit 1 +} + +@test "api-keys-limits: USD wallet payments also respect limits" { + # Create API key with daily limit for USD wallet tests + key_name="$(new_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-usd-secret" "$secret" + + # Set daily limit to 50000 sats (in satoshi equivalent) + 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 + + # Verify spending was recorded (converted to sats) + 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" { + # Create new API key with daily limit + key_name="$(new_key_name)" + 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" + + # Set daily limit to 8000 sats + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":8000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Create no-amount invoice + 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 + + # Verify spending was recorded + 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 message contains limit information + error_msg="$(graphql_output '.data.lnNoAmountInvoicePaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} + +@test "api-keys-limits: lnNoAmountUsdInvoicePaymentSend respects limits" { + # Create new API key with daily limit + key_name="$(new_key_name)" + 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" + + # Set daily limit to 8000 sats + variables="{\"input\":{\"id\":\"${key_id}\",\"limitTimeWindow\":\"DAILY\",\"limitSats\":8000}}" + exec_graphql 'alice' 'api-key-set-limit' "$variables" + + # Create no-amount invoice + 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 + + # Verify spending was recorded (converted to sats) + exec_graphql 'alice' 'api-keys' + spent_24h="$(graphql_output '.data.me.apiKeys[] | select(.name == "'$(read_value 'ln_noamount_usd_key_name')'") | .limits.dailySpentSats')" + [[ "${spent_24h}" -gt "0" ]] || exit 1 +} + +@test "api-keys-limits: lnurlPaymentSend respects limits" { + # Create new API key with daily limit + key_name="$(new_key_name)" + 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" + + # Set daily limit to 5000 sats + 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 + + # Verify spending was recorded + 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 message contains limit information + error_msg="$(graphql_output '.data.lnurlPaymentSend.errors[0].message')" + [[ "${error_msg}" == *"daily"* ]] || exit 1 +} \ No newline at end of file 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-747d3d564d63f5aeeac3db15cff84bebe11d59fd343c083b745f9ce4ee09ed0d.json b/core/api-keys/.sqlx/query-747d3d564d63f5aeeac3db15cff84bebe11d59fd343c083b745f9ce4ee09ed0d.json new file mode 100644 index 0000000000..6439ef21bf --- /dev/null +++ b/core/api-keys/.sqlx/query-747d3d564d63f5aeeac3db15cff84bebe11d59fd343c083b745f9ce4ee09ed0d.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 ON CONFLICT (transaction_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "747d3d564d63f5aeeac3db15cff84bebe11d59fd343c083b745f9ce4ee09ed0d" +} diff --git a/core/api-keys/.sqlx/query-87c9634aa38f95eb1f850b1901153f9cf2e2299c9a4dd5a9e111b2cb1400d283.json b/core/api-keys/.sqlx/query-87c9634aa38f95eb1f850b1901153f9cf2e2299c9a4dd5a9e111b2cb1400d283.json new file mode 100644 index 0000000000..3217e80dd9 --- /dev/null +++ b/core/api-keys/.sqlx/query-87c9634aa38f95eb1f850b1901153f9cf2e2299c9a4dd5a9e111b2cb1400d283.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT 1 as \"exists!\"\n FROM api_key_transactions\n WHERE transaction_id = $1\n AND api_key_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "87c9634aa38f95eb1f850b1901153f9cf2e2299c9a4dd5a9e111b2cb1400d283" +} diff --git a/core/api-keys/.sqlx/query-9c69eb1f61d4e4832bbe570fc3214d9958cb372c32088c0bbf7dbc9b4c6a897b.json b/core/api-keys/.sqlx/query-9c69eb1f61d4e4832bbe570fc3214d9958cb372c32088c0bbf7dbc9b4c6a897b.json new file mode 100644 index 0000000000..399e702bd4 --- /dev/null +++ b/core/api-keys/.sqlx/query-9c69eb1f61d4e4832bbe570fc3214d9958cb372c32088c0bbf7dbc9b4c6a897b.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": "9c69eb1f61d4e4832bbe570fc3214d9958cb372c32088c0bbf7dbc9b4c6a897b" +} 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-ce601eb27ddb4b76e7da627a1400915ad3673025f5bacd07b056a66efcb85b06.json b/core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json similarity index 65% rename from core/api-keys/.sqlx/query-ce601eb27ddb4b76e7da627a1400915ad3673025f5bacd07b056a66efcb85b06.json rename to core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json index a12fbb75a3..1908eefb7c 100644 --- a/core/api-keys/.sqlx/query-ce601eb27ddb4b76e7da627a1400915ad3673025f5bacd07b056a66efcb85b06.json +++ b/core/api-keys/.sqlx/query-eb63f3d930396dbcdc2f1fae84837a482cab3bec0207c9e9d5df0f2f4e71c66a.json @@ -1,6 +1,6 @@ { "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 ON CONFLICT (transaction_id) DO NOTHING\n ", + "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": { @@ -12,5 +12,5 @@ }, "nullable": [] }, - "hash": "ce601eb27ddb4b76e7da627a1400915ad3673025f5bacd07b056a66efcb85b06" + "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..33d3b5ee03 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,18 @@ 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()) + } + _ => 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..027b0be721 100644 --- a/core/api-keys/src/limits/error.rs +++ b/core/api-keys/src/limits/error.rs @@ -7,4 +7,13 @@ pub enum LimitError { #[error("Invalid limit amount (must be positive)")] InvalidLimitAmount, + + #[error("Missing transaction id for ephemeral finalization")] + MissingTransactionId, + + #[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..74b7d77cc4 100644 --- a/core/api-keys/src/limits/mod.rs +++ b/core/api-keys/src/limits/mod.rs @@ -62,7 +62,7 @@ impl Limits { api_key_id: IdentityApiKeyId, amount_sats: i64, ) -> Result { - if amount_sats < 0 { + if amount_sats <= 0 { return Err(LimitError::InvalidLimitAmount); } @@ -94,30 +94,157 @@ impl Limits { }) } - #[tracing::instrument(name = "limits.record_spending", skip(self))] - pub async fn record_spending( + #[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, - transaction_id: Option, - ) -> Result<(), LimitError> { + ) -> Result { if amount_sats <= 0 { return Err(LimitError::InvalidLimitAmount); } + let mut tx = self.pool.begin().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 { + tx.rollback().await?; + 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()) - ON CONFLICT (transaction_id) DO NOTHING "#, api_key_id as IdentityApiKeyId, amount_sats, - transaction_id, + &ephemeral_id, ) - .execute(&self.pool) + .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); + } + + match ephemeral_id { + Some(eid) => { + let txn_id = transaction_id.ok_or(LimitError::MissingTransactionId)?; + + let result = sqlx::query!( + r#" + UPDATE api_key_transactions + SET transaction_id = $1 + WHERE transaction_id = $2 + AND api_key_id = $3 + "#, + &txn_id, + &eid, + api_key_id as IdentityApiKeyId, + ) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + // Check if already finalized (idempotent retry) + let already_recorded = sqlx::query_scalar!( + r#" + SELECT 1 as "exists!" + FROM api_key_transactions + WHERE transaction_id = $1 + AND api_key_id = $2 + "#, + &txn_id, + api_key_id as IdentityApiKeyId, + ) + .fetch_optional(&self.pool) + .await?; + + if already_recorded.is_none() { + return Err(LimitError::EphemeralNotFound(eid)); + } + } + } + None => { + sqlx::query!( + r#" + INSERT INTO api_key_transactions (api_key_id, amount_sats, transaction_id, created_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (transaction_id) DO NOTHING + "#, + api_key_id as IdentityApiKeyId, + amount_sats, + transaction_id, + ) + .execute(&self.pool) + .await?; + } + } + Ok(()) } @@ -454,17 +581,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 +661,32 @@ 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))); + } } 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/send-intraledger.ts b/core/api/src/app/payments/send-intraledger.ts index af7a768dfa..18310146d1 100644 --- a/core/api/src/app/payments/send-intraledger.ts +++ b/core/api/src/app/payments/send-intraledger.ts @@ -44,8 +44,10 @@ import { WalletsRepository, } from "@/services/mongoose" import { NotificationsService } from "@/services/notifications" +import { ApiKeysService } from "@/services/api-keys" const dealer = DealerPriceService() +const apiKeys = ApiKeysService() const intraledgerPaymentSendWalletId = async ({ recipientWalletId: uncheckedRecipientWalletId, @@ -53,6 +55,7 @@ const intraledgerPaymentSendWalletId = async ({ amount: uncheckedAmount, memo, senderWalletId: uncheckedSenderWalletId, + apiKeyId, }: IntraLedgerPaymentSendWalletIdArgs): Promise => { const validatedPaymentInputs = await validateIntraledgerPaymentInputs({ uncheckedSenderWalletId, @@ -120,6 +123,16 @@ const intraledgerPaymentSendWalletId = async ({ "payment.finalRecipient": JSON.stringify(paymentFlow.recipientWalletDescriptor()), }) + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderAccount, @@ -128,7 +141,17 @@ const intraledgerPaymentSendWalletId = async ({ recipientUser, senderUser, memo, + apiKeyId, + ephemeralId, }) + + if (paymentSendResult instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } + if (paymentSendResult instanceof Error) return paymentSendResult if (senderAccount.id !== recipientAccount.id) { @@ -231,6 +254,8 @@ const executePaymentViaIntraledger = async < recipientUser, senderUser, memo, + apiKeyId, + ephemeralId, }: { paymentFlow: PaymentFlow senderAccount: Account @@ -239,6 +264,8 @@ const executePaymentViaIntraledger = async < recipientUser: User senderUser: User memo: string | null + apiKeyId?: ApiKeyId + ephemeralId?: EphemeralId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, @@ -318,6 +345,18 @@ const executePaymentViaIntraledger = async < transaction: senderWalletTransaction, }) + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + transactionId: journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } + return { status: PaymentSendStatus.Success, transaction: senderWalletTransaction, diff --git a/core/api/src/app/payments/send-lightning.ts b/core/api/src/app/payments/send-lightning.ts index 57dea4ad31..cbf84467a4 100644 --- a/core/api/src/app/payments/send-lightning.ts +++ b/core/api/src/app/payments/send-lightning.ts @@ -54,6 +54,7 @@ import { DealerPriceService } from "@/services/dealer-price" import { LedgerService } from "@/services/ledger" import { LockService } from "@/services/lock" import { NotificationsService } from "@/services/notifications" +import { ApiKeysService } from "@/services/api-keys" import * as LedgerFacade from "@/services/ledger/facade" import { @@ -78,6 +79,7 @@ import { import { ResourceExpiredLockServiceError } from "@/domain/lock" const dealer = DealerPriceService() +const apiKeys = ApiKeysService() const paymentFlowRepo = PaymentFlowStateRepository(defaultTimeToExpiryInSeconds) export const payInvoiceByWalletId = async ({ @@ -85,6 +87,7 @@ export const payInvoiceByWalletId = async ({ memo, senderWalletId: uncheckedSenderWalletId, senderAccount, + apiKeyId, }: PayInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, @@ -116,12 +119,31 @@ export const payInvoiceByWalletId = async ({ } = validatedPaymentInputs if (paymentFlow.settlementMethod !== SettlementMethod.IntraLedger) { - return executePaymentViaLn({ + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + + const paymentSendResult = await executePaymentViaLn({ decodedInvoice, paymentFlow, senderAccount, memo, + apiKeyId, + ephemeralId, }) + if (paymentSendResult instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } + return paymentSendResult } const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() @@ -138,13 +160,31 @@ export const payInvoiceByWalletId = async ({ const accountValidator = AccountValidator(recipientAccount) if (accountValidator instanceof Error) return accountValidator + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderWalletId, senderAccount, recipientAccount, memo, + apiKeyId, + ephemeralId, }) + if (paymentSendResult instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } if (paymentSendResult instanceof Error) return paymentSendResult if (senderAccount.id !== recipientAccount.id) { @@ -166,6 +206,7 @@ const payNoAmountInvoiceByWalletId = async ({ memo, senderWalletId: uncheckedSenderWalletId, senderAccount, + apiKeyId, }: PayNoAmountInvoiceByWalletIdArgs): Promise => { addAttributesToCurrentSpan({ "payment.initiation_method": PaymentInitiationMethod.Lightning, @@ -199,12 +240,31 @@ const payNoAmountInvoiceByWalletId = async ({ } = validatedNoAmountPaymentInputs if (paymentFlow.settlementMethod !== SettlementMethod.IntraLedger) { - return executePaymentViaLn({ + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + + const paymentSendResult = await executePaymentViaLn({ decodedInvoice, paymentFlow, senderAccount, memo, + apiKeyId, + ephemeralId, }) + if (paymentSendResult instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } + return paymentSendResult } const { walletDescriptor: recipientWalletDescriptor } = paymentFlow.recipientDetails() @@ -222,13 +282,31 @@ const payNoAmountInvoiceByWalletId = async ({ const accountValidator = AccountValidator(recipientAccount) if (accountValidator instanceof Error) return accountValidator + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + const paymentSendResult = await executePaymentViaIntraledger({ paymentFlow, senderWalletId, senderAccount, recipientAccount, memo, + apiKeyId, + ephemeralId, }) + if (paymentSendResult instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } if (paymentSendResult instanceof Error) return paymentSendResult if (senderAccount.id !== recipientAccount.id) { @@ -432,12 +510,16 @@ const executePaymentViaIntraledger = async < senderWalletId, recipientAccount, memo, + apiKeyId, + ephemeralId, }: { paymentFlow: PaymentFlow senderAccount: Account senderWalletId: WalletId recipientAccount: Account memo: string | null + apiKeyId?: ApiKeyId + ephemeralId?: EphemeralId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.IntraLedger, @@ -546,6 +628,18 @@ const executePaymentViaIntraledger = async < transaction: senderWalletTransaction, }) + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + transactionId: journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } + return { status: PaymentSendStatus.Success, transaction: senderWalletTransaction, @@ -727,11 +821,15 @@ const executePaymentViaLn = async ({ paymentFlow, senderAccount, memo, + apiKeyId, + ephemeralId, }: { decodedInvoice: LnInvoice paymentFlow: PaymentFlow senderAccount: Account memo: string | null + apiKeyId?: ApiKeyId + ephemeralId?: EphemeralId }): Promise => { addAttributesToCurrentSpan({ "payment.settlement_method": SettlementMethod.Lightning, @@ -801,6 +899,17 @@ const executePaymentViaLn = async ({ return paymentSendAttemptResult.error case PaymentSendAttemptResultType.Pending: + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + transactionId: paymentSendAttemptResult.journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } return getPendingPaymentResponse({ walletId: senderWalletId, paymentHash, @@ -813,6 +922,18 @@ const executePaymentViaLn = async ({ }) default: + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + transactionId: paymentSendAttemptResult.journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } + return { status: PaymentSendStatus.Success, transaction: walletTransaction, 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..eab0cebfc6 100644 --- a/core/api/src/app/payments/send-on-chain.ts +++ b/core/api/src/app/payments/send-on-chain.ts @@ -51,10 +51,15 @@ import { WalletsRepository, } from "@/services/mongoose" import { NotificationsService } from "@/services/notifications" -import { addAttributesToCurrentSpan } from "@/services/tracing" +import { + addAttributesToCurrentSpan, + recordExceptionInCurrentSpan, +} from "@/services/tracing" +import { ApiKeysService } from "@/services/api-keys" const { dustThreshold } = getOnChainWalletConfig() const dealer = DealerPriceService() +const apiKeys = ApiKeysService() const payOnChainByWalletId = async ({ senderAccount, @@ -65,6 +70,7 @@ const payOnChainByWalletId = async ({ speed, memo, sendAll, + apiKeyId, }: PayOnChainByWalletIdArgs): Promise => { const latestAccountState = await AccountsRepository().findById(senderAccount.id) if (latestAccountState instanceof Error) return latestAccountState @@ -173,12 +179,36 @@ const payOnChainByWalletId = async ({ .withAmount(amount) .withConversion(withConversionArgs) - return executePaymentViaIntraledger({ + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const proposedAmounts = await builder.proposedAmounts() + if (proposedAmounts instanceof Error) return proposedAmounts + + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: proposedAmounts.btc, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + + const result = await executePaymentViaIntraledger({ builder, senderAccount, memo, sendAll, + apiKeyId, + ephemeralId, }) + + if (result instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } + + return result } const builder = withSenderBuilder @@ -186,14 +216,38 @@ const payOnChainByWalletId = async ({ .withAmount(amount) .withConversion(withConversionArgs) - return executePaymentViaOnChain({ + let ephemeralId: EphemeralId | undefined + if (apiKeyId) { + const proposedAmounts = await builder.proposedAmounts() + if (proposedAmounts instanceof Error) return proposedAmounts + + const lockResult = await apiKeys.checkAndLockSpending({ + apiKeyId, + amount: proposedAmounts.btc, + }) + if (lockResult instanceof Error) return lockResult + ephemeralId = lockResult + } + + const result = await executePaymentViaOnChain({ builder, senderDisplayCurrency: senderAccount.displayCurrency, speed, memo, sendAll, logger: onchainLogger, + apiKeyId, + ephemeralId, }) + + if (result instanceof Error && ephemeralId) { + const reverseResult = await apiKeys.reverseSpending({ transactionId: ephemeralId }) + if (reverseResult instanceof Error) { + recordExceptionInCurrentSpan({ error: reverseResult }) + } + } + + return result } export const payOnChainByWalletIdForBtcWallet = async ( @@ -248,11 +302,15 @@ const executePaymentViaIntraledger = async < senderAccount, memo, sendAll, + apiKeyId, + ephemeralId, }: { builder: OPFBWithConversion | OPFBWithError senderAccount: Account memo: string | null sendAll: boolean + apiKeyId?: ApiKeyId + ephemeralId?: EphemeralId }): Promise => { const paymentFlow = await builder.withoutMinerFee() if (paymentFlow instanceof Error) return paymentFlow @@ -357,6 +415,18 @@ const executePaymentViaIntraledger = async < transaction: senderWalletTransaction, }) + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: paymentFlow.btcPaymentAmount, + transactionId: journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } + return { status: PaymentSendStatus.Success, transaction: senderWalletTransaction, @@ -523,6 +593,8 @@ const executePaymentViaOnChain = async < memo, sendAll, logger, + apiKeyId, + ephemeralId, }: { builder: OPFBWithConversion | OPFBWithError senderDisplayCurrency: DisplayCurrency @@ -530,6 +602,8 @@ const executePaymentViaOnChain = async < memo: string | null sendAll: boolean logger: Logger + apiKeyId?: ApiKeyId + ephemeralId?: EphemeralId }): Promise => { const senderWalletDescriptor = await builder.senderWalletDescriptor() if (senderWalletDescriptor instanceof Error) return senderWalletDescriptor @@ -572,6 +646,18 @@ const executePaymentViaOnChain = async < }) if (walletTransaction instanceof Error) return walletTransaction + if (apiKeyId && ephemeralId) { + const recordResult = await apiKeys.recordSpending({ + apiKeyId, + amount: proposedAmounts.btc, + transactionId: journalId, + ephemeralId, + }) + if (recordResult instanceof Error) { + recordExceptionInCurrentSpan({ error: recordResult }) + } + } + return { status: PaymentSendStatus.Success, transaction: walletTransaction } } diff --git a/core/api/src/app/payments/update-pending-payments.ts b/core/api/src/app/payments/update-pending-payments.ts index e7c0e9b74c..e81a748f36 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" @@ -43,6 +44,8 @@ import { } from "@/services/tracing" import { runInParallel } from "@/utils" +const apiKeys = ApiKeysService() + export const updatePendingPayments = async (logger: Logger): Promise => { const ledgerService = LedgerService() const walletIdsWithPendingPayments = ledgerService.listWalletIdsWithPendingPayments() @@ -358,7 +361,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 +392,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..f81dab244c 100644 --- a/core/api/src/config/index.ts +++ b/core/api/src/config/index.ts @@ -84,6 +84,9 @@ export const getCallbackServiceConfig = (): SvixConfig => { export const getBriaConfig = getBriaPartialConfigFromYaml +export const API_KEYS_HOST = env.API_KEYS_HOST +export const API_KEYS_PORT = env.API_KEYS_PORT + export const isTelegramPassportEnabled = () => !!env.TELEGRAM_BOT_API_TOKEN && !!env.TELEGRAM_PASSPORT_PRIVATE_KEY 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..1514420817 --- /dev/null +++ b/core/api/src/domain/api-keys/index.types.d.ts @@ -0,0 +1,21 @@ +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: string }): Promise +} diff --git a/core/api/src/graphql/error-map.ts b/core/api/src/graphql/error-map.ts index 025a294941..0d7385efb6 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 }) @@ -615,6 +631,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 +842,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 +904,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/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..ed3b4c7df4 100644 --- a/core/api/src/servers/middlewares/session.ts +++ b/core/api/src/servers/middlewares/session.ts @@ -30,6 +30,7 @@ export const sessionPublicContext = async ({ ) const sub = tokenPayload?.sub const appId = tokenPayload?.client_id + const apiKeyId = tokenPayload?.api_key_id as ApiKeyId | undefined // note: value should match (ie: "anon") if not an accountId // settings from dev/ory/oathkeeper.yml/authenticator/anonymous/config/subjet @@ -87,6 +88,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..109db199e5 --- /dev/null +++ b/core/api/src/services/api-keys/errors.ts @@ -0,0 +1,40 @@ +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): + 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\)/, + InvalidLimitError: /Invalid limit value/, + DatabaseError: /Database/, +} 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..ee51bf0271 --- /dev/null +++ b/core/api/src/services/api-keys/index.ts @@ -0,0 +1,97 @@ +import { + CheckAndLockSpendingRequest, + RecordSpendingRequest, + ReverseSpendingRequest, +} from "./proto/api_keys_pb" + +import * as apiKeysGrpc from "./grpc-client" + +import { handleCommonApiKeysErrors } from "./errors" + +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 = Number(amount.amount) + 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 = Number(amount.amount) + 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: string + }): 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..addcc3c669 --- /dev/null +++ b/core/api/src/services/api-keys/proto/buf.gen.yaml @@ -0,0 +1,16 @@ +# /proto/buf.gen.yaml +version: v1 + +plugins: + - name: js + out: . + opt: import_style=commonjs,binary + path: ../../../../node_modules/.bin/protoc-gen-js + - name: grpc + out: . + opt: grpc_js + path: ../../../../node_modules/.bin/grpc_tools_node_protoc_plugin + - name: ts + out: . + opt: grpc_js + path: ../../../../node_modules/.bin/protoc-gen-ts
NameAPI Key IDScopeExpires AtLast UsedActionNameAPI Key IDScopeBudget LimitsExpires AtLast UsedActions
{name} {id} {getScopeText(scopes)} + {hasAnyLimit ? ( + + {dailyLimitSats && ( +
+ + Daily: {dailyLimitSats.toLocaleString()} sats + + + Spent: {dailySpentSats.toLocaleString()} / Remaining:{" "} + {remainingDailyLimitSats?.toLocaleString() || 0} + +
+ )} + {weeklyLimitSats && ( +
+ + Weekly: {weeklyLimitSats.toLocaleString()}{" "} + sats + + + Spent: {weeklySpentSats.toLocaleString()} / Remaining:{" "} + {(weeklyLimitSats - weeklySpentSats).toLocaleString()} + +
+ )} + {monthlyLimitSats && ( +
+ + Monthly: {monthlyLimitSats.toLocaleString()}{" "} + sats + + + Spent: {monthlySpentSats.toLocaleString()} / Remaining:{" "} + {(monthlyLimitSats - monthlySpentSats).toLocaleString()} + +
+ )} + {annualLimitSats && ( +
+ + Annual: {annualLimitSats.toLocaleString()}{" "} + sats + + + Spent: {annualSpentSats.toLocaleString()} / Remaining:{" "} + {(annualLimitSats - annualSpentSats).toLocaleString()} + +
+ )} +
+ ) : ( + + Unlimited + + )} +
{expiresAt ? formatDate(expiresAt) : "Never"} {lastUsedAt ? formatDate(lastUsedAt) : "Never"} - + + + +