Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add username usd suffix handling #4009

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 49 additions & 11 deletions bats/core/api/public-ln-receive.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions bats/gql/account-default-wallet-by-username.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
query accountDefaultWalletByUsername(
$username: UsernameWithFlags!
$walletCurrency: WalletCurrency
) {
accountDefaultWalletByUsername(
username: $username
walletCurrency: $walletCurrency
) {
id
currency
}
}
6 changes: 0 additions & 6 deletions bats/gql/account-default-wallet.gql

This file was deleted.

7 changes: 6 additions & 1 deletion core/api/dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down
11 changes: 11 additions & 0 deletions core/api/src/domain/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from "./limits-checker"
export * from "./limits-volume"
export * from "./account-validator"
export * from "./primitives"
export * from "./username-parser"

const KratosUserIdRegex = UUIDV4
const AccountIdRegex = UUIDV4
Expand Down Expand Up @@ -68,6 +69,7 @@ export const checkedAccountStatus = (status: string) => {
}

export const UsernameRegex = /(?!^(1|3|bc1|lnbc1))^[0-9a-z_]{3,50}$/i
export const UsernameWithFlagsRegex = /(?!^(1|3|bc1|lnbc1))^[0-9a-z+_]{3,50}$/i

export const checkedToUsername = (username: string): Username | ValidationError => {
if (!username.match(UsernameRegex)) {
Expand All @@ -76,6 +78,15 @@ export const checkedToUsername = (username: string): Username | ValidationError
return username as Username
}

export const checkedToUsernameWithFlags = (
username: string,
): Username | ValidationError => {
if (!username.match(UsernameWithFlagsRegex)) {
return new InvalidUsername(username)
}
return username as Username
}

export const ContactAliasRegex = /^[0-9A-Za-z_]{3,50}$/i

export const checkedToContactAlias = (alias: string): ContactAlias | ValidationError => {
Expand Down
26 changes: 26 additions & 0 deletions core/api/src/domain/accounts/username-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { checkedToUsername } from "."

export const UsernameParser = (username: UsernameWithFlags) => {
const stripUsdFlag = () => checkedToUsername(username.slice(0, -4))
const isUsdCheck = () => username.slice(-4).toLowerCase() === "+usd"

const isUsd = () => {
const usdCheck = isUsdCheck()
if (usdCheck) {
const strippedUsername = stripUsdFlag()
if (strippedUsername instanceof Error) return strippedUsername
}

return usdCheck
}

const parsedUsername = (): Username | ValidationError => {
const usdCheck = isUsdCheck()
return usdCheck ? stripUsdFlag() : checkedToUsername(username)
}

return {
isUsd,
parsedUsername,
}
}
1 change: 1 addition & 0 deletions core/api/src/domain/primitives/index.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type DisplayCurrencyPerSat = number & { readonly brand: unique symbol }
type DisplayCurrencyBasePerSat = number & { readonly brand: unique symbol }
type Username = string & { readonly brand: unique symbol }
type UsernameWithFlags = string & { readonly brand: unique symbol }
type Pubkey = string & { readonly brand: unique symbol }
type WalletId = string & { readonly brand: unique symbol }
type AccountId = string & { readonly brand: unique symbol }
Expand Down
2 changes: 2 additions & 0 deletions core/api/src/graphql/public/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Wallets } from "@/app"

import { CouldNotFindWalletFromUsernameAndCurrencyError } from "@/domain/errors"

import { AccountsRepository } from "@/services/mongoose"

import { mapError } from "@/graphql/error-map"
import { GT } from "@/graphql/index"
import Username from "@/graphql/shared/types/scalar/username"
import WalletCurrency from "@/graphql/shared/types/scalar/wallet-currency"
import PublicWallet from "@/graphql/public/types/object/public-wallet"
import { AccountsRepository } from "@/services/mongoose"

const AccountDefaultWalletQuery = GT.Field({
deprecationReason: "will be migrated to AccountDefaultWalletIdByUsername",
type: GT.NonNull(PublicWallet),
args: {
username: {
Expand Down
6 changes: 5 additions & 1 deletion core/api/src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!]!
Expand Down Expand Up @@ -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.
"""
Expand Down
35 changes: 35 additions & 0 deletions core/api/src/graphql/shared/types/scalar/username-with-flags.ts
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions core/api/test/unit/domain/accounts/username-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { UsernameParser } from "@/domain/accounts"
import { InvalidUsername } from "@/domain/errors"

describe("UsernameParser", () => {
describe("isUsd", () => {
it("parses a '+usd' flag", () => {
const parser = UsernameParser("user+usd" as UsernameWithFlags)
expect(parser.parsedUsername()).toEqual("user")
expect(parser.isUsd()).toBeTruthy()
})

it("parses a non-'+usd' flag", () => {
const parser = UsernameParser("user" as UsernameWithFlags)
expect(parser.parsedUsername()).toEqual("user")
expect(parser.isUsd()).toBeFalsy()
})

it("errors for a short username with '+usd' flag", () => {
const parser = UsernameParser("bo+usd" as UsernameWithFlags)
expect(parser.parsedUsername()).toBeInstanceOf(InvalidUsername)
expect(parser.isUsd()).toBeInstanceOf(InvalidUsername)
})
})
})
Loading
Loading