From 9968417ae160ccbf446b68c125d4d8393b266f85 Mon Sep 17 00:00:00 2001 From: vindard <17693119+vindard@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:08:37 -0400 Subject: [PATCH] feat(core): add new 'AccountDefaultWallet' with UsernameWithFlags type --- bats/core/api/public-ln-receive.bats | 60 +++++++++++++--- .../account-default-wallet-by-username.gql | 12 ++++ bats/gql/account-default-wallet.gql | 6 -- .../dev/apollo-federation/supergraph.graphql | 7 +- core/api/src/graphql/public/queries.ts | 2 + .../account-default-wallet-by-username.ts | 72 +++++++++++++++++++ .../root/query/account-default-wallet.ts | 14 +--- core/api/src/graphql/public/schema.graphql | 6 +- .../types/scalar/username-with-flags.ts | 35 +++++++++ .../apollo-federation/supergraph.graphql | 7 +- 10 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 bats/gql/account-default-wallet-by-username.gql delete mode 100644 bats/gql/account-default-wallet.gql create mode 100644 core/api/src/graphql/public/root/query/account-default-wallet-by-username.ts create mode 100644 core/api/src/graphql/shared/types/scalar/username-with-flags.ts diff --git a/bats/core/api/public-ln-receive.bats b/bats/core/api/public-ln-receive.bats index a75031a646..f02c0923f3 100644 --- a/bats/core/api/public-ln-receive.bats +++ b/bats/core/api/public-ln-receive.bats @@ -47,8 +47,8 @@ usd_amount=50 --arg username "$username" \ '{username: $username}' ) - exec_graphql 'anon' 'account-default-wallet' "$variables" - receiver_wallet_id="$(graphql_output '.data.accountDefaultWallet.id')" + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" [[ "$receiver_wallet_id" == "$(read_value $btc_wallet_name)" ]] || exit 1 # Fetch usd-wallet-id from username @@ -57,8 +57,8 @@ usd_amount=50 --arg username "$username" \ '{username: $username, walletCurrency: "USD"}' ) - exec_graphql 'anon' 'account-default-wallet' "$variables" - receiver_wallet_id="$(graphql_output '.data.accountDefaultWallet.id')" + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" [[ "$receiver_wallet_id" == "$(read_value $usd_wallet_name)" ]] || exit 1 } @@ -84,8 +84,8 @@ usd_amount=50 --arg username "$username" \ '{username: $username}' ) - exec_graphql 'anon' 'account-default-wallet' "$variables" - receiver_wallet_id="$(graphql_output '.data.accountDefaultWallet.id')" + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" [[ "$receiver_wallet_id" == "$(read_value $usd_wallet_name)" ]] || exit 1 # Fetch btc-wallet-id from username @@ -94,17 +94,55 @@ usd_amount=50 --arg username "$username" \ '{username: $username, walletCurrency: "BTC"}' ) - exec_graphql 'anon' 'account-default-wallet' "$variables" - receiver_wallet_id="$(graphql_output '.data.accountDefaultWallet.id')" + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" [[ "$receiver_wallet_id" == "$(read_value $btc_wallet_name)" ]] || exit 1 } +@test "public-ln-receive: account details - can fetch with usd flag from username" { + token_name=$ALICE + btc_wallet_name="$token_name.btc_wallet_id" + usd_wallet_name="$token_name.usd_wallet_id" + local username="$(read_value $token_name.username)" + + # Change default wallet to btc + variables=$( + jq -n \ + --arg wallet_id "$(read_value $btc_wallet_name)" \ + '{input: {walletId: $wallet_id}}' + ) + exec_graphql "$token_name" 'account-update-default-wallet-id' "$variables" + updated_wallet_id="$(graphql_output '.data.accountUpdateDefaultWalletId.account.defaultWalletId')" + [[ "$updated_wallet_id" == "$(read_value $btc_wallet_name)" ]] || exit 1 + + # Fetch default btc-wallet-id from username + variables=$( + jq -n \ + --arg username "$username" \ + '{username: $username}' + ) + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" + [[ "$receiver_wallet_id" == "$(read_value $btc_wallet_name)" ]] || exit 1 + + # Fetch usd-wallet-id from username via '+usd' flag + variables=$( + jq -n \ + --arg username "$username+usd" \ + '{username: $username}' + ) + exec_graphql 'anon' 'account-default-wallet-by-username' "$variables" + receiver_wallet_id="$(graphql_output '.data.accountDefaultWalletByUsername.id')" + [[ "$receiver_wallet_id" == "$(read_value $usd_wallet_name)" ]] || exit 1 +} + @test "public-ln-receive: account details - return error for invalid username" { - exec_graphql 'anon' 'account-default-wallet' '{"username": "incorrectly-formatted"}' + exec_graphql 'anon' 'account-default-wallet-by-username' '{"username": "incorrectly-formatted"}' + graphql_output error_msg="$(graphql_output '.errors[0].message')" - [[ "$error_msg" == "Invalid value for Username" ]] || exit 1 + [[ "$error_msg" == "Invalid value for Username with optional flags" ]] || exit 1 - exec_graphql 'anon' 'account-default-wallet' '{"username": "idontexist"}' + exec_graphql 'anon' 'account-default-wallet-by-username' '{"username": "idontexist"}' error_msg="$(graphql_output '.errors[0].message')" [[ "$error_msg" == "Account does not exist for username idontexist" ]] || exit 1 } diff --git a/bats/gql/account-default-wallet-by-username.gql b/bats/gql/account-default-wallet-by-username.gql new file mode 100644 index 0000000000..204f1d4e24 --- /dev/null +++ b/bats/gql/account-default-wallet-by-username.gql @@ -0,0 +1,12 @@ +query accountDefaultWalletByUsername( + $username: UsernameWithFlags! + $walletCurrency: WalletCurrency +) { + accountDefaultWalletByUsername( + username: $username + walletCurrency: $walletCurrency + ) { + id + currency + } +} diff --git a/bats/gql/account-default-wallet.gql b/bats/gql/account-default-wallet.gql deleted file mode 100644 index bd00e3b846..0000000000 --- a/bats/gql/account-default-wallet.gql +++ /dev/null @@ -1,6 +0,0 @@ -query accountDefaultWallet($username: Username!, $walletCurrency: WalletCurrency) { - accountDefaultWallet(username: $username, walletCurrency: $walletCurrency) { - id - currency - } -} diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index 309fcb25dd..09d7e0a88e 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -1562,7 +1562,8 @@ type Query @join__type(graph: NOTIFICATIONS) @join__type(graph: PUBLIC) { - accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) + accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) @deprecated(reason: "will be migrated to AccountDefaultWalletIdByUsername") + accountDefaultWalletByUsername(username: UsernameWithFlags!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) btcPriceList(range: PriceGraphRange!): [PricePoint] @join__field(graph: PUBLIC) businessMapMarkers: [MapMarker!]! @join__field(graph: PUBLIC) currencyList: [Currency!]! @join__field(graph: PUBLIC) @@ -2079,6 +2080,10 @@ input UserLogoutInput scalar Username @join__type(graph: PUBLIC) +"""Unique identifier of a user, with optional flags""" +scalar UsernameWithFlags + @join__type(graph: PUBLIC) + enum UserNotificationCategory @join__type(graph: NOTIFICATIONS) { diff --git a/core/api/src/graphql/public/queries.ts b/core/api/src/graphql/public/queries.ts index 05bebbba3c..a62d02ddf3 100644 --- a/core/api/src/graphql/public/queries.ts +++ b/core/api/src/graphql/public/queries.ts @@ -12,6 +12,7 @@ import OnChainUsdTxFeeAsBtcDenominatedQuery from "@/graphql/public/root/query/on import UsernameAvailableQuery from "@/graphql/public/root/query/username-available" import BusinessMapMarkersQuery from "@/graphql/public/root/query/business-map-markers" import AccountDefaultWalletQuery from "@/graphql/public/root/query/account-default-wallet" +import AccountDefaultWalletByUsernameQuery from "@/graphql/public/root/query/account-default-wallet-by-username" import AccountDefaultWalletIdQuery from "@/graphql/public/root/query/account-default-wallet-id" import LnInvoicePaymentStatusQuery from "@/graphql/public/root/query/ln-invoice-payment-status" import LnInvoicePaymentStatusByHashQuery from "@/graphql/public/root/query/ln-invoice-payment-status-by-hash" @@ -23,6 +24,7 @@ export const queryFields = { usernameAvailable: UsernameAvailableQuery, userDefaultWalletId: AccountDefaultWalletIdQuery, // FIXME: migrate to AccountDefaultWalletId accountDefaultWallet: AccountDefaultWalletQuery, + accountDefaultWalletByUsername: AccountDefaultWalletByUsernameQuery, businessMapMarkers: BusinessMapMarkersQuery, currencyList: CurrencyListQuery, mobileVersions: MobileVersionsQuery, diff --git a/core/api/src/graphql/public/root/query/account-default-wallet-by-username.ts b/core/api/src/graphql/public/root/query/account-default-wallet-by-username.ts new file mode 100644 index 0000000000..ebf007892a --- /dev/null +++ b/core/api/src/graphql/public/root/query/account-default-wallet-by-username.ts @@ -0,0 +1,72 @@ +import { Wallets } from "@/app" + +import { UsernameParser } from "@/domain/accounts" +import { WalletCurrency as DomainWalletCurrency } from "@/domain/shared" +import { CouldNotFindWalletFromUsernameAndCurrencyError } from "@/domain/errors" + +import { AccountsRepository } from "@/services/mongoose" + +import { mapError } from "@/graphql/error-map" +import { GT } from "@/graphql/index" +import UsernameWithFlags from "@/graphql/shared/types/scalar/username-with-flags" +import WalletCurrency from "@/graphql/shared/types/scalar/wallet-currency" +import PublicWallet from "@/graphql/public/types/object/public-wallet" + +const AccountDefaultWalletByUsernameQuery = GT.Field({ + type: GT.NonNull(PublicWallet), + args: { + username: { + type: GT.NonNull(UsernameWithFlags), + }, + walletCurrency: { type: WalletCurrency }, + }, + resolve: async (_, args) => { + const { username: usernameWithFlags, walletCurrency } = args + + if (usernameWithFlags instanceof Error) { + throw usernameWithFlags + } + + const parser = UsernameParser(usernameWithFlags) + const username = parser.parsedUsername() + if (username instanceof Error) { + throw mapError(username) + } + + const account = await AccountsRepository().findByUsername(username) + if (account instanceof Error) { + throw mapError(account) + } + + const wallets = await Wallets.listWalletsByAccountId(account.id) + if (wallets instanceof Error) { + throw mapError(wallets) + } + + if (parser.isUsd()) { + const wallet = wallets.find( + (wallet) => wallet.currency === DomainWalletCurrency.Usd, + ) + if (!wallet) { + throw mapError(new CouldNotFindWalletFromUsernameAndCurrencyError(username)) + } + + return wallet + } + + if (!walletCurrency) { + return wallets.find((wallet) => wallet.id === account.defaultWalletId) + } + + const wallet = wallets.find((wallet) => wallet.currency === walletCurrency) + if (!wallet) { + throw mapError( + new CouldNotFindWalletFromUsernameAndCurrencyError(usernameWithFlags), + ) + } + + return wallet + }, +}) + +export default AccountDefaultWalletByUsernameQuery diff --git a/core/api/src/graphql/public/root/query/account-default-wallet.ts b/core/api/src/graphql/public/root/query/account-default-wallet.ts index 02a9d4a5e4..a329feaaba 100644 --- a/core/api/src/graphql/public/root/query/account-default-wallet.ts +++ b/core/api/src/graphql/public/root/query/account-default-wallet.ts @@ -1,7 +1,5 @@ import { Wallets } from "@/app" -import { UsernameParser } from "@/domain/accounts" -import { WalletCurrency as DomainWalletCurrency } from "@/domain/shared" import { CouldNotFindWalletFromUsernameAndCurrencyError } from "@/domain/errors" import { AccountsRepository } from "@/services/mongoose" @@ -13,6 +11,7 @@ import WalletCurrency from "@/graphql/shared/types/scalar/wallet-currency" import PublicWallet from "@/graphql/public/types/object/public-wallet" const AccountDefaultWalletQuery = GT.Field({ + deprecationReason: "will be migrated to AccountDefaultWalletIdByUsername", type: GT.NonNull(PublicWallet), args: { username: { @@ -37,17 +36,6 @@ const AccountDefaultWalletQuery = GT.Field({ throw mapError(wallets) } - if (UsernameParser(username).isUsd()) { - const wallet = wallets.find( - (wallet) => wallet.currency === DomainWalletCurrency.Usd, - ) - if (!wallet) { - throw mapError(new CouldNotFindWalletFromUsernameAndCurrencyError(username)) - } - - return wallet - } - if (!walletCurrency) { return wallets.find((wallet) => wallet.id === account.defaultWalletId) } diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index eb54b8836d..079807deff 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -1204,7 +1204,8 @@ type PublicWallet { } type Query { - accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! + accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! @deprecated(reason: "will be migrated to AccountDefaultWalletIdByUsername") + accountDefaultWalletByUsername(username: UsernameWithFlags!, walletCurrency: WalletCurrency): PublicWallet! btcPriceList(range: PriceGraphRange!): [PricePoint] businessMapMarkers: [MapMarker!]! currencyList: [Currency!]! @@ -1667,6 +1668,9 @@ type UserUpdateUsernamePayload { """Unique identifier of a user""" scalar Username +"""Unique identifier of a user, with optional flags""" +scalar UsernameWithFlags + """ A generic wallet which stores value in one of our supported currencies. """ diff --git a/core/api/src/graphql/shared/types/scalar/username-with-flags.ts b/core/api/src/graphql/shared/types/scalar/username-with-flags.ts new file mode 100644 index 0000000000..8533c12adf --- /dev/null +++ b/core/api/src/graphql/shared/types/scalar/username-with-flags.ts @@ -0,0 +1,35 @@ +import { UsernameWithFlagsRegex } from "@/domain/accounts" +import { InputValidationError } from "@/graphql/error" +import { GT } from "@/graphql/index" + +const UsernameWithFlags = GT.Scalar({ + name: "UsernameWithFlags", + description: "Unique identifier of a user, with optional flags", + parseValue(value) { + if (typeof value !== "string") { + return new InputValidationError({ + message: "Invalid type for Username with optional flags", + }) + } + return validUsernameWithFlagsValue(value) + }, + parseLiteral(ast) { + if (ast.kind === GT.Kind.STRING) { + return validUsernameWithFlagsValue(ast.value) + } + return new InputValidationError({ + message: "Invalid type for Username with optional flags", + }) + }, +}) + +function validUsernameWithFlagsValue(value: string) { + if (value.match(UsernameWithFlagsRegex)) { + return value.toLowerCase() + } + return new InputValidationError({ + message: "Invalid value for Username with optional flags", + }) +} + +export default UsernameWithFlags diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 309fcb25dd..09d7e0a88e 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -1562,7 +1562,8 @@ type Query @join__type(graph: NOTIFICATIONS) @join__type(graph: PUBLIC) { - accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) + accountDefaultWallet(username: Username!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) @deprecated(reason: "will be migrated to AccountDefaultWalletIdByUsername") + accountDefaultWalletByUsername(username: UsernameWithFlags!, walletCurrency: WalletCurrency): PublicWallet! @join__field(graph: PUBLIC) btcPriceList(range: PriceGraphRange!): [PricePoint] @join__field(graph: PUBLIC) businessMapMarkers: [MapMarker!]! @join__field(graph: PUBLIC) currencyList: [Currency!]! @join__field(graph: PUBLIC) @@ -2079,6 +2080,10 @@ input UserLogoutInput scalar Username @join__type(graph: PUBLIC) +"""Unique identifier of a user, with optional flags""" +scalar UsernameWithFlags + @join__type(graph: PUBLIC) + enum UserNotificationCategory @join__type(graph: NOTIFICATIONS) {