Skip to content

Commit

Permalink
Add LOBSTR vault support (#1055)
Browse files Browse the repository at this point in the history
* Add LobstrVault icon

* Add 3rd-party multi-factor support

* Show badge for 3rd-party service in accounts list

* Show badge for 3rd-party service in account title

* Conditionally send transaction to 3rd-party service
  • Loading branch information
ebma authored Jun 24, 2020
1 parent a7ba3f2 commit 66a31da
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 88 deletions.
2 changes: 1 addition & 1 deletion i18n/locales/en/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@
},
"tooltip": {
"multi-sig-account": "Multi-Signature Account",
"stellarguard": "StellarGuard Protection"
"security-service": "{{service}} Protection"
},
"placeholder": "Account name…",
"testnet": "Testnet"
Expand Down
4 changes: 2 additions & 2 deletions i18n/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
},
"badges": {
"tooltip": {
"stellar-guard": "StellarGuard Protection",
"multi-sig": "Multi-Signature Account"
"multi-sig": "Multi-Signature Account",
"security-service": "{{service}} Protection"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
32 changes: 13 additions & 19 deletions src/Account/components/AccountTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
Expand All @@ -73,10 +72,14 @@ export const StaticBadges = React.memo(function StaticBadges(props: StaticBadges
<GroupIcon style={{ fontSize: "120%", marginRight: 8 }} />
</Tooltip>
)
} else if (props.multisig === "stellar-guard") {
} else if (props.multisig) {
return (
<Tooltip title={t("account.title.tooltip.stellarguard")}>
<StellarGuardIcon style={{ fontSize: "80%", marginRight: 8 }} />
<Tooltip
title={t("account.title.tooltip.security-service", `${props.multisig.name} Protection`, {
service: props.multisig.name
})}
>
{props.multisig.icon({ style: { fontSize: "80%", marginRight: 8 } })}
</Tooltip>
)
} else {
Expand All @@ -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 (
<StaticBadges
multisig={
accountData.signers.length > 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 <StaticBadges multisig={multisig} password={props.account.requiresPassword} testnet={props.account.testnet} />
})

const useTitleTextfieldStyles = makeStyles({
Expand Down
16 changes: 11 additions & 5 deletions src/App/components/AccountList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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) ? (
<Tooltip title={t("app.account-list.badges.tooltip.stellar-guard")}>
<StellarGuardIcon style={{ marginTop: 6 }} />

const securityService = containsThirdPartySigner(accountData.signers)

const multiSigIcon = securityService ? (
<Tooltip
title={t("app.account-list.badges.tooltip.security-service", `${securityService.name} Protection`, {
service: securityService.name
})}
>
{securityService.icon({ style: { marginTop: 6 } })}
</Tooltip>
) : (
<Tooltip title={t("app.account-list.badges.tooltip.multi-sig")}>
Expand Down
53 changes: 0 additions & 53 deletions src/Generic/lib/stellar-guard.ts

This file was deleted.

97 changes: 97 additions & 0 deletions src/Generic/lib/third-party-security.ts
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions src/Icons/components/LobstrVault.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SvgIcon viewBox="0 0 67 96" {...props}>
<defs>
<linearGradient id="linearGradient-1" x1="74.917%" x2="0%" y1="62.329%" y2="37.418%">
<stop offset="0%"></stop>
<stop offset="100%" stopColor="#090114" stopOpacity="0"></stop>
</linearGradient>
<linearGradient id="linearGradient-2" x1="74.917%" x2="0%" y1="88.177%" y2="11.038%">
<stop offset="0%"></stop>
<stop offset="100%" stopColor="#090114" stopOpacity="0"></stop>
</linearGradient>
</defs>
<g fill="none" fillRule="evenodd" stroke="none" strokeWidth="1">
<g transform="translate(-28 -13)">
<g transform="translate(28.467 13.217)">
<path
fill="url(#linearGradient-1)"
fillOpacity="0.31"
d="M49.081 26.925C48.876 14.83 43.518 8.783 33.008 8.783c-10.51 0-15.846 6.047-16.006 18.142 1.795-10.747 7.13-16.12 16.006-16.12 8.875 0 14.233 5.373 16.073 16.12z"
opacity="0.8"
></path>
<path
fill="currentColor"
d="M7.822 37.256c-.069-.847.03-.139 0-.71C6.562 12.183 14.968 0 33.042 0c17.906 0 26.323 11.958 25.252 35.875-.063 1.391-.157-.092-.284 1.38m-41.102 0c-.115-1.876-.185-.345-.21-1.234-.502-17.976 4.946-26.964 16.344-26.964 11.436 0 16.882 9.048 16.338 27.145-.038 1.277-.107-.315-.205 1.054"
opacity="0.9"
></path>
<path
fill="url(#linearGradient-2)"
fillOpacity="0.31"
d="M7.566 81.078C4.074 77.425.873 73.968.265 65.39c-.353-4.985-.353-14.36 0-28.128l32.777-7.457 32.776 7.457c.354 13.767.354 23.143 0 28.128-.608 8.58-3.809 12.036-7.3 15.69-4.573 4.784-13.065 9.613-25.476 14.488-12.411-4.875-20.903-9.704-25.476-14.489z"
opacity="0.8"
></path>
<path
fill="currentColor"
d="M7.566 78.552C4.074 75.08.873 71.793.265 63.636c-.353-4.739-.353-13.653 0-26.742l32.777-7.09 32.776 7.09c.354 13.089.354 22.003 0 26.742-.584 7.844-3.566 11.183-6.899 14.516l-.401.4c-4.573 4.55-13.065 9.14-25.476 13.775-12.411-4.634-20.903-9.226-25.476-13.775z"
opacity="0.8"
></path>
<path
fill="currentColor"
d="M33.042 29.804l32.776 7.09c.354 13.089.354 22.003 0 26.742-.608 8.157-3.809 11.443-7.3 14.916-4.573 4.55-13.065 9.14-25.476 13.775V29.804z"
opacity="0.5"
></path>
<path
fill="currentColor"
d="M33.0416667 29.8038418L33.0416667 34.9796962 -4.97379915e-14 41.6836021 0.0925333327 36.881422z"
opacity="0.9"
></path>
<path
fill="currentColor"
d="M65.9334084 29.8038418L65.9334084 34.9796962 33.0416667 41.6836021 33.148808 36.8893969z"
opacity="0.9"
transform="matrix(-1 0 0 1 98.975 0)"
></path>
</g>
</g>
</g>
</SvgIcon>
)
}

export default LobstrVaultIcon
4 changes: 2 additions & 2 deletions src/Transaction/components/SubmissionProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 66a31da

Please sign in to comment.