Skip to content

Commit ce2ca4b

Browse files
committed
Add mainnet forking support
1 parent 69de8fd commit ce2ca4b

7 files changed

Lines changed: 322 additions & 48 deletions

File tree

components/AccountsList.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import accountGenerator from "src/accountGenerator"
88
import {Box, Themed} from "theme-ui"
99
import {SXStyles} from "types"
1010
import FormErrors from "./FormErrors"
11+
import useConfig from "hooks/useConfig"
1112

1213
const styles: SXStyles = {
1314
accountCreated: {
@@ -33,19 +34,22 @@ const styles: SXStyles = {
3334
export default function AccountsList({
3435
accounts,
3536
onEditAccount,
37+
onUseAnyAccount,
3638
createdAccountAddress,
3739
flowAccountAddress,
3840
flowAccountPrivateKey,
3941
avatarUrl,
4042
}: {
4143
accounts: Account[]
4244
onEditAccount: (account: Account | NewAccount) => void
45+
onUseAnyAccount: () => void
4346
createdAccountAddress: string | null
4447
flowAccountAddress: string
4548
flowAccountPrivateKey: string
4649
avatarUrl: string
4750
}) {
4851
const {initError} = useAuthnContext()
52+
const {forkMode} = useConfig()
4953

5054
return (
5155
<div>
@@ -87,6 +91,14 @@ export default function AccountsList({
8791
>
8892
Create New Account
8993
</PlusButton>
94+
{forkMode && (
95+
<PlusButton
96+
onClick={onUseAnyAccount}
97+
data-test="use-any-account-button"
98+
>
99+
Use Existing Address (Fork Mode)
100+
</PlusButton>
101+
)}
90102
</Box>
91103
<Themed.hr sx={{mt: 0, mb: 4}} />
92104
</>

components/AnyAccountForm.tsx

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/** @jsxImportSource theme-ui */
2+
import ConnectedAppHeader from "components/ConnectedAppHeader"
3+
import {styles as dialogStyles} from "components/Dialog"
4+
import {Field, Form, Formik} from "formik"
5+
import {useState} from "react"
6+
import {Account} from "src/accounts"
7+
import {getBaseUrl} from "src/utils"
8+
import {Box} from "theme-ui"
9+
import {SXStyles} from "types"
10+
import useAuthnContext from "hooks/useAuthnContext"
11+
import Button from "./Button"
12+
import {CustomInputComponent} from "./Inputs"
13+
import {chooseAccount} from "src/accountAuth"
14+
15+
const styles: SXStyles = {
16+
form: {
17+
position: "relative",
18+
},
19+
backButton: {
20+
background: "none",
21+
border: 0,
22+
position: "absolute",
23+
top: 0,
24+
left: 0,
25+
cursor: "pointer",
26+
p: 0,
27+
zIndex: 10,
28+
},
29+
actionsContainer: {
30+
display: "flex",
31+
alignItems: "center",
32+
width: "100%",
33+
borderTop: "1px solid",
34+
borderColor: "gray.200",
35+
backgroundColor: "white",
36+
borderBottomLeftRadius: 10,
37+
borderBottomRightRadius: 10,
38+
px: [10, 20],
39+
},
40+
actions: {
41+
display: "flex",
42+
flex: 1,
43+
pt: 20,
44+
pb: 20,
45+
},
46+
}
47+
48+
type FormValues = {
49+
address: string
50+
keyId: string
51+
label: string
52+
}
53+
54+
export default function AnyAccountForm({
55+
onCancel,
56+
flowAccountAddress,
57+
flowAccountPrivateKey,
58+
avatarUrl,
59+
}: {
60+
onCancel: () => void
61+
flowAccountAddress: string
62+
flowAccountPrivateKey: string
63+
avatarUrl: string
64+
}) {
65+
const baseUrl = getBaseUrl()
66+
const {connectedAppConfig, appScopes} = useAuthnContext()
67+
const [submitting, setSubmitting] = useState(false)
68+
const [error, setError] = useState<string | null>(null)
69+
70+
return (
71+
<Formik<FormValues>
72+
initialValues={{
73+
address: "",
74+
keyId: "0",
75+
label: "",
76+
}}
77+
validate={values => {
78+
const errors: Record<string, string> = {}
79+
if (!values.address) errors.address = "Address is required"
80+
if (values.keyId === "" || isNaN(Number(values.keyId)))
81+
errors.keyId = "Key ID must be a number"
82+
return errors
83+
}}
84+
onSubmit={async values => {
85+
setError(null)
86+
setSubmitting(true)
87+
try {
88+
const account: Account = {
89+
type: "ACCOUNT",
90+
address: values.address,
91+
keyId: Number(values.keyId),
92+
label: values.label || values.address,
93+
scopes: appScopes,
94+
}
95+
96+
await chooseAccount(
97+
baseUrl,
98+
flowAccountPrivateKey,
99+
account,
100+
new Set(appScopes),
101+
connectedAppConfig
102+
)
103+
} catch (e: unknown) {
104+
setError(String(e))
105+
setSubmitting(false)
106+
}
107+
}}
108+
>
109+
{({submitForm, errors: formErrors}) => (
110+
<>
111+
<div sx={dialogStyles.body}>
112+
<Form sx={styles.form}>
113+
<Button onClick={onCancel} sx={styles.backButton}>
114+
<img src="/back-arrow.svg" />
115+
</Button>
116+
117+
<Box mb={4}>
118+
<ConnectedAppHeader
119+
info={false}
120+
title={"Use Existing Address (Fork Mode)"}
121+
description={
122+
"Authenticate as any address on the forked network."
123+
}
124+
flowAccountAddress={flowAccountAddress}
125+
avatarUrl={avatarUrl}
126+
/>
127+
</Box>
128+
129+
<Box mb={4}>
130+
<Field
131+
component={CustomInputComponent}
132+
inputLabel="Address"
133+
name="address"
134+
placeholder="0x..."
135+
required
136+
/>
137+
</Box>
138+
139+
<Box mb={5}>
140+
<Field
141+
component={CustomInputComponent}
142+
inputLabel="Key ID"
143+
name="keyId"
144+
placeholder="0"
145+
required
146+
/>
147+
</Box>
148+
149+
<Box mb={5}>
150+
<Field
151+
component={CustomInputComponent}
152+
inputLabel="Label (optional)"
153+
name="label"
154+
placeholder="External account label"
155+
/>
156+
</Box>
157+
158+
{error && <div sx={{color: "red", fontSize: 1}}>{error}</div>}
159+
{Object.values(formErrors).length > 0 && (
160+
<div sx={{color: "red", fontSize: 1}}>
161+
{Object.values(formErrors).join(". ")}
162+
</div>
163+
)}
164+
</Form>
165+
</div>
166+
<div sx={dialogStyles.footer}>
167+
<div sx={styles.actionsContainer}>
168+
<div sx={styles.actions}>
169+
<Button
170+
onClick={onCancel}
171+
type="button"
172+
variant="ghost"
173+
block
174+
size="lg"
175+
sx={{flex: 1, mr: 10, w: "50%"}}
176+
>
177+
Cancel
178+
</Button>
179+
<Button
180+
type="button"
181+
block
182+
size="lg"
183+
sx={{flex: 1, ml: 10, w: "50%"}}
184+
disabled={submitting || Object.values(formErrors).length > 0}
185+
onClick={submitForm}
186+
>
187+
Authenticate
188+
</Button>
189+
</div>
190+
</div>
191+
</div>
192+
</>
193+
)}
194+
</Formik>
195+
)
196+
}

contexts/ConfigContext.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import React, {createContext, useEffect, useState} from "react"
2+
import * as fcl from "@onflow/fcl"
3+
import {
4+
CHAIN_ID_MAINNET,
5+
CHAIN_ID_TESTNET,
6+
SERVICE_ACCOUNT_MAINNET,
7+
SERVICE_ACCOUNT_TESTNET,
8+
} from "src/constants"
29
import fclConfig from "src/fclConfig"
310
import {Spinner} from "../components/Spinner"
411
import {getBaseUrl} from "src/utils"
@@ -12,6 +19,7 @@ interface RuntimeConfig {
1219
flowAccessNode: string
1320
flowInitAccountsNo: number
1421
flowInitAccountBalance: string
22+
forkMode: boolean
1523
}
1624

1725
const defaultConfig = {
@@ -23,13 +31,45 @@ const defaultConfig = {
2331
flowAccessNode: process.env.flowAccessNode || "",
2432
flowInitAccountsNo: parseInt(process.env.flowInitAccountsNo || "0") || 0,
2533
flowInitAccountBalance: process.env.flowInitAccountBalance || "1000.0",
34+
forkMode: false,
2635
}
2736

28-
export const ConfigContext = createContext<RuntimeConfig>(defaultConfig)
37+
export const ConfigContext = createContext<RuntimeConfig>(defaultConfig as RuntimeConfig)
38+
39+
async function detectForkAndOverride(config: RuntimeConfig): Promise<RuntimeConfig> {
40+
let chainId = ""
41+
try {
42+
// Queries the configured access node
43+
chainId = (await (fcl as any).getChainId()) || ""
44+
} catch (e) {
45+
chainId = ""
46+
}
47+
48+
const isMainnet = chainId === CHAIN_ID_MAINNET
49+
const isTestnet = chainId === CHAIN_ID_TESTNET
50+
const forkMode = isMainnet || isTestnet
51+
52+
if (!forkMode) return {...config, forkMode: false}
53+
54+
const SERVICE_ACCOUNT_BY_CHAIN: Record<string, string> = {
55+
[CHAIN_ID_MAINNET]: SERVICE_ACCOUNT_MAINNET,
56+
[CHAIN_ID_TESTNET]: SERVICE_ACCOUNT_TESTNET,
57+
}
58+
59+
const serviceAccount = SERVICE_ACCOUNT_BY_CHAIN[chainId]
60+
61+
return {
62+
...config,
63+
forkMode: true,
64+
// Force service account to known network service account in fork mode
65+
flowAccountAddress: serviceAccount,
66+
}
67+
}
2968

3069
async function getConfig(): Promise<RuntimeConfig> {
3170
if (process.env.isLocal) {
32-
return replaceAccessUrlBaseUrl(defaultConfig)
71+
const cfg = replaceAccessUrlBaseUrl(defaultConfig as RuntimeConfig)
72+
return cfg as RuntimeConfig
3373
}
3474

3575
const result = await fetch(`${getBaseUrl()}/api/`)
@@ -74,11 +114,10 @@ export function ConfigContextProvider({children}: {children: React.ReactNode}) {
74114
useEffect(() => {
75115
async function fetchConfig() {
76116
const config = await getConfig()
77-
78117
const {flowAccessNode} = config
79-
80118
fclConfig(flowAccessNode)
81-
setConfig(config)
119+
const finalized = await detectForkAndOverride(config)
120+
setConfig(finalized)
82121
}
83122

84123
fetchConfig()

go/wallet/bundle.zip

3.29 KB
Binary file not shown.

0 commit comments

Comments
 (0)