diff --git a/i18n/locales/en/account.json b/i18n/locales/en/account.json
index d39f91e3a..d325d16ea 100644
--- a/i18n/locales/en/account.json
+++ b/i18n/locales/en/account.json
@@ -326,7 +326,7 @@
},
"tooltip": {
"multi-sig-account": "Multi-Signature Account",
- "stellarguard": "StellarGuard Protection"
+ "security-service": "{{service}} Protection"
},
"placeholder": "Account nameā¦",
"testnet": "Testnet"
diff --git a/i18n/locales/en/app.json b/i18n/locales/en/app.json
index 686e44532..5cc469d67 100644
--- a/i18n/locales/en/app.json
+++ b/i18n/locales/en/app.json
@@ -5,8 +5,8 @@
},
"badges": {
"tooltip": {
- "stellar-guard": "StellarGuard Protection",
- "multi-sig": "Multi-Signature Account"
+ "multi-sig": "Multi-Signature Account",
+ "security-service": "{{service}} Protection"
}
}
},
diff --git a/i18n/locales/en/generic.json b/i18n/locales/en/generic.json
index 25905fc79..dfadc9534 100644
--- a/i18n/locales/en/generic.json
+++ b/i18n/locales/en/generic.json
@@ -31,6 +31,7 @@
"stellar-address-not-found-error": "Stellar address not found: {{address}}",
"stellar-address-request-failed-error": "Stellar address resolution of {{address}} failed.",
"submission-failed-error": "Submitting transaction to {{endpoint}} failed with status {{status}}: {{message}}",
+ "testnet-endpoint-not-available-error": "{{service}} does not provide a testnet endpoint.",
"unexpected-action-error": "Unexpected action: {{action}}",
"unexpected-state-error": "Encountered unexpected state: {{state}}",
"unexpected-response-type-error": "Unexpected response type: {{type}} / ${dataType}",
diff --git a/src/Account/components/AccountTitle.tsx b/src/Account/components/AccountTitle.tsx
index 739fb5e13..31560f1d8 100644
--- a/src/Account/components/AccountTitle.tsx
+++ b/src/Account/components/AccountTitle.tsx
@@ -13,9 +13,8 @@ import VerifiedUserIcon from "@material-ui/icons/VerifiedUser"
import { Account } from "~App/contexts/accounts"
import { useLiveAccountData } from "~Generic/hooks/stellar-subscriptions"
import { useIsMobile, useRouter } from "~Generic/hooks/userinterface"
-import { containsStellarGuardAsSigner } from "~Generic/lib/stellar-guard"
+import { containsThirdPartySigner, ThirdPartySecurityService } from "~Generic/lib/third-party-security"
import { primaryBackgroundColor } from "~App/theme"
-import StellarGuardIcon from "~Icons/components/StellarGuard"
import { HorizontalLayout } from "~Layout/components/Box"
import MainTitle from "~Generic/components/MainTitle"
@@ -56,7 +55,7 @@ function TestnetBadge(props: { style?: React.CSSProperties }) {
}
interface StaticBadgesProps {
- multisig: "generic" | "stellar-guard" | undefined
+ multisig: "generic" | ThirdPartySecurityService | undefined
password: boolean
testnet: boolean
}
@@ -73,10 +72,14 @@ export const StaticBadges = React.memo(function StaticBadges(props: StaticBadges
)
- } else if (props.multisig === "stellar-guard") {
+ } else if (props.multisig) {
return (
-
-
+
+ {props.multisig.icon({ style: { fontSize: "80%", marginRight: 8 } })}
)
} else {
@@ -95,19 +98,10 @@ interface BadgesProps {
export const Badges = React.memo(function Badges(props: BadgesProps) {
const accountData = useLiveAccountData(props.account.publicKey, props.account.testnet)
- return (
- 1
- ? containsStellarGuardAsSigner(accountData.signers)
- ? "stellar-guard"
- : "generic"
- : undefined
- }
- password={props.account.requiresPassword}
- testnet={props.account.testnet}
- />
- )
+ const securityService = containsThirdPartySigner(accountData.signers)
+ const multisig = accountData.signers.length > 1 ? (securityService ? securityService : "generic") : undefined
+
+ return
})
const useTitleTextfieldStyles = makeStyles({
diff --git a/src/App/components/AccountList.tsx b/src/App/components/AccountList.tsx
index 488299abf..1130661a9 100644
--- a/src/App/components/AccountList.tsx
+++ b/src/App/components/AccountList.tsx
@@ -14,10 +14,9 @@ import { Account } from "../contexts/accounts"
import { SignatureDelegationContext } from "../contexts/signatureDelegation"
import { useLiveAccountData } from "~Generic/hooks/stellar-subscriptions"
import { useRouter } from "~Generic/hooks/userinterface"
-import { containsStellarGuardAsSigner } from "~Generic/lib/stellar-guard"
+import { containsThirdPartySigner } from "~Generic/lib/third-party-security"
import { SignatureRequest } from "~Generic/lib/multisig-service"
import InlineLoader from "~Generic/components/InlineLoader"
-import StellarGuardIcon from "~Icons/components/StellarGuard"
import { Box, HorizontalLayout, VerticalLayout } from "~Layout/components/Box"
import * as routes from "../routes"
@@ -71,9 +70,16 @@ const StyledBadge = (props: BadgeProps) => {
function Badges(props: { account: Account }) {
const { t } = useTranslation()
const accountData = useLiveAccountData(props.account.publicKey, props.account.testnet)
- const multiSigIcon = containsStellarGuardAsSigner(accountData.signers) ? (
-
-
+
+ const securityService = containsThirdPartySigner(accountData.signers)
+
+ const multiSigIcon = securityService ? (
+
+ {securityService.icon({ style: { marginTop: 6 } })}
) : (
diff --git a/src/Generic/lib/stellar-guard.ts b/src/Generic/lib/stellar-guard.ts
deleted file mode 100644
index 8b5afbcd2..000000000
--- a/src/Generic/lib/stellar-guard.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Server, Transaction, Horizon } from "stellar-sdk"
-import { CustomError } from "./errors"
-
-const STELLARGUARD_TRANSACTION_ENDPOINT_MAINNET = "https://stellarguard.me/api/transactions"
-const STELLARGUARD_TRANSACTION_ENDPOINT_TESTNET = "https://test.stellarguard.me/api/transactions"
-const STELLARGUARD_PUBLIC_KEY = "GCVHEKSRASJBD6O2Z532LWH4N2ZLCBVDLLTLKSYCSMBLOYTNMEEGUARD"
-
-export async function isStellarGuardProtected(horizon: Server, accountPubKey: string) {
- const account = await horizon.loadAccount(accountPubKey)
- return account.signers.some(signer => signer.key === STELLARGUARD_PUBLIC_KEY)
-}
-
-export function containsStellarGuardAsSigner(signers: Horizon.AccountSigner[]) {
- return signers.some(signer => signer.key === STELLARGUARD_PUBLIC_KEY)
-}
-
-export async function submitTransactionToStellarGuard(signedTransaction: Transaction, testnet: boolean) {
- const signedTransactionXDR = signedTransaction
- .toEnvelope()
- .toXDR()
- .toString("base64")
-
- const body = { xdr: signedTransactionXDR }
-
- const endpoint = testnet ? STELLARGUARD_TRANSACTION_ENDPOINT_TESTNET : STELLARGUARD_TRANSACTION_ENDPOINT_MAINNET
-
- const response = await fetch(endpoint, {
- method: "POST",
- body: JSON.stringify(body),
- headers: {
- "Content-Type": "application/json"
- }
- })
-
- if (!response.ok) {
- const contentType = response.headers.get("Content-Type")
- const responseBodyObject = contentType && contentType.startsWith("application/json") ? await response.json() : null
-
- const message =
- responseBodyObject && responseBodyObject.message ? responseBodyObject.message : await response.text()
- throw CustomError(
- "SubmissionFailedError",
- `Submitting transaction to StellarGuard failed with status ${response.status}: ${message}`,
- {
- endpoint: "Stellarguard",
- message,
- status: String(response.status)
- }
- )
- }
-
- return response
-}
diff --git a/src/Generic/lib/third-party-security.ts b/src/Generic/lib/third-party-security.ts
new file mode 100644
index 000000000..bd46e8e17
--- /dev/null
+++ b/src/Generic/lib/third-party-security.ts
@@ -0,0 +1,97 @@
+import { Server, Transaction, Horizon } from "stellar-sdk"
+import { CustomError } from "./errors"
+import StellarGuardIcon from "~Icons/components/StellarGuard"
+import LobstrVaultIcon from "~Icons/components/LobstrVault"
+
+export interface ThirdPartySecurityService {
+ endpoints: {
+ testnet?: string
+ mainnet: string
+ }
+ icon: (props: { style: React.CSSProperties }) => JSX.Element
+ name: string
+ publicKey: string
+}
+
+const services: ThirdPartySecurityService[] = [
+ {
+ endpoints: {
+ mainnet: "https://stellarguard.me/api/transactions",
+ testnet: "https://test.stellarguard.me/api/transactions"
+ },
+ icon: StellarGuardIcon,
+ name: "StellarGuard",
+ publicKey: "GCVHEKSRASJBD6O2Z532LWH4N2ZLCBVDLLTLKSYCSMBLOYTNMEEGUARD"
+ },
+ {
+ endpoints: {
+ mainnet: "https://vault.lobstr.co/api/transactions/"
+ },
+ icon: LobstrVaultIcon,
+ name: "LOBSTR Vault",
+ publicKey: "GA2T6GR7VXXXBETTERSAFETHANSORRYXXXPROTECTEDBYLOBSTRVAULT"
+ }
+]
+
+export async function isThirdPartyProtected(horizon: Server, accountPubKey: string) {
+ const account = await horizon.loadAccount(accountPubKey)
+ const signerKeys = account.signers.map(signer => signer.key)
+
+ const enabledService = services.find(service => signerKeys.includes(service.publicKey))
+ return enabledService
+}
+
+export function containsThirdPartySigner(signers: Horizon.AccountSigner[]) {
+ const signerKeys = signers.map(signer => signer.key)
+
+ const enabledService = services.find(service => signerKeys.includes(service.publicKey))
+ return enabledService
+}
+
+export async function submitTransactionToThirdPartyService(
+ signedTransaction: Transaction,
+ service: ThirdPartySecurityService,
+ testnet: boolean
+) {
+ const signedTransactionXDR = signedTransaction
+ .toEnvelope()
+ .toXDR()
+ .toString("base64")
+
+ const body = { xdr: signedTransactionXDR }
+
+ if (testnet && !service.endpoints.testnet) {
+ throw CustomError("TestnetEndpointNotAvailableError", `${service.name} does not provide a testnet endpoint.`, {
+ service: service.name
+ })
+ }
+
+ const endpoint = testnet ? service.endpoints.testnet! : service.endpoints.mainnet
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ body: JSON.stringify(body),
+ headers: {
+ "Content-Type": "application/json"
+ }
+ })
+
+ if (!response.ok) {
+ const contentType = response.headers.get("Content-Type")
+ const responseBodyObject = contentType && contentType.startsWith("application/json") ? await response.json() : null
+
+ const message =
+ responseBodyObject && responseBodyObject.message ? responseBodyObject.message : await response.text()
+ throw CustomError(
+ "SubmissionFailedError",
+ `Submitting transaction to ${service.name} failed with status ${response.status}: ${message}`,
+ {
+ endpoint: service.name,
+ message,
+ status: String(response.status)
+ }
+ )
+ }
+
+ return response
+}
diff --git a/src/Icons/components/LobstrVault.tsx b/src/Icons/components/LobstrVault.tsx
new file mode 100644
index 000000000..e49d19f9a
--- /dev/null
+++ b/src/Icons/components/LobstrVault.tsx
@@ -0,0 +1,70 @@
+import * as React from "react"
+import SvgIcon from "@material-ui/core/SvgIcon"
+
+function LobstrVaultIcon(props: {
+ className?: string
+ onClick?: () => void
+ role?: string
+ style?: React.CSSProperties
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default LobstrVaultIcon
diff --git a/src/Transaction/components/SubmissionProgress.tsx b/src/Transaction/components/SubmissionProgress.tsx
index be91c5da7..6ffd61796 100644
--- a/src/Transaction/components/SubmissionProgress.tsx
+++ b/src/Transaction/components/SubmissionProgress.tsx
@@ -30,13 +30,13 @@ function Heading(props: { children: React.ReactNode }) {
export enum SubmissionType {
default,
multisig,
- stellarguard
+ thirdParty
}
const successMessages: { [type: number]: string } = {
[SubmissionType.default]: "Successful",
[SubmissionType.multisig]: "Waiting for missing signatures",
- [SubmissionType.stellarguard]: "Waiting for StellarGuard authorization"
+ [SubmissionType.thirdParty]: "Waiting for authorization of third-party service"
}
interface SubmissionProgressProps {
diff --git a/src/Transaction/components/TransactionSender.tsx b/src/Transaction/components/TransactionSender.tsx
index 5a6f9aa95..e4763e7c6 100644
--- a/src/Transaction/components/TransactionSender.tsx
+++ b/src/Transaction/components/TransactionSender.tsx
@@ -17,7 +17,11 @@ import {
} from "~Generic/lib/multisig-service"
import { networkPassphrases } from "~Generic/lib/stellar"
import { hasSigned, requiresRemoteSignatures, signTransaction } from "~Generic/lib/transaction"
-import { isStellarGuardProtected, submitTransactionToStellarGuard } from "~Generic/lib/stellar-guard"
+import {
+ isThirdPartyProtected,
+ submitTransactionToThirdPartyService,
+ ThirdPartySecurityService
+} from "~Generic/lib/third-party-security"
import { workers } from "~Workers/worker-controller"
import TransactionReviewDialog from "~TransactionReview/components/TransactionReviewDialog"
import SubmissionProgress, { SubmissionType } from "./SubmissionProgress"
@@ -186,8 +190,9 @@ class TransactionSender extends React.Component {
}
try {
- if (await isStellarGuardProtected(horizon, account.publicKey)) {
- await this.submitTransactionToStellarGuard(signedTx)
+ const thirdPartySecurityService = await isThirdPartyProtected(horizon, account.publicKey)
+ if (thirdPartySecurityService) {
+ await this.submitTransactionToThirdPartyService(signedTx, thirdPartySecurityService)
} else if (await requiresRemoteSignatures(horizon, signedTx, account.publicKey)) {
await this.submitTransactionToMultisigService(signedTx)
} else {
@@ -251,12 +256,12 @@ class TransactionSender extends React.Component {
}
}
- submitTransactionToStellarGuard = async (signedTransaction: Transaction) => {
+ submitTransactionToThirdPartyService = async (signedTransaction: Transaction, service: ThirdPartySecurityService) => {
try {
- const promise = submitTransactionToStellarGuard(signedTransaction, this.props.account.testnet)
+ const promise = submitTransactionToThirdPartyService(signedTransaction, service, this.props.account.testnet)
this.setSubmissionPromise(promise)
- this.setState({ submissionType: SubmissionType.stellarguard })
+ this.setState({ submissionType: SubmissionType.thirdParty })
return await promise
} catch (error) {
throw explainSubmissionErrorResponse(error, this.props.t)