Skip to content

Commit bb579ba

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

28 files changed

Lines changed: 2626 additions & 158 deletions

‎cadence/contracts/FCL.cdc‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ access(all) contract FCL {
8585
return acct
8686
}
8787

88+
access(all) fun add(address: Address, label: String, scopes: [String]) {
89+
let acct = FCLAccount(address: address, label: label, scopes: scopes)
90+
self.account.storage.borrow<&Root>(from: self.storagePath)!.add(acct)
91+
}
92+
8893
access(all) fun update(address: Address, label: String, scopes: [String]) {
8994
self.account.storage.borrow<&Root>(from: self.storagePath)!
9095
.update(address: address, label: label, scopes: scopes)

‎cadence/scripts/getAccounts.cdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import "FCL"
1+
import FCL from 0xFCL
22

33
access(all) fun main(): &[FCL.FCLAccount] {
44
return FCL.accounts().values
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import FCL from 0xFCL
2+
3+
transaction(address: Address, label: String, scopes: [String]) {
4+
prepare(acct: &Account) {
5+
FCL.add(address: address, label: label, scopes: scopes)
6+
}
7+
}
8+

‎cadence/transactions/fundFLOW.cdc‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import "FlowToken"
2-
import "FungibleToken"
1+
import FlowToken from 0xFlowToken
2+
import FungibleToken from 0xFungibleToken
33

44
transaction(address: Address, amount: UFix64) {
55
let tokenAdmin: &FlowToken.Administrator

‎cadence/transactions/newAccount.cdc‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import "FCL"
2-
import "FlowToken"
3-
import "FungibleToken"
1+
import FCL from 0xFCL
2+
import FlowToken from 0xFlowToken
3+
import FungibleToken from 0xFungibleToken
44

55
/// This transaction creates a new account and funds it with
66
/// an initial balance of FLOW tokens.

‎cadence/transactions/updateAccount.cdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import "FCL"
1+
import FCL from 0xFCL
22

33
transaction(address: Address, label: String, scopes: [String]) {
44
prepare() {

‎components/AccountsList.tsx‎

Lines changed: 23 additions & 2 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: {
@@ -19,9 +20,13 @@ const styles: SXStyles = {
1920
mb: 3,
2021
},
2122
plusButtonContainer: {
22-
height: 90,
2323
display: "flex",
2424
alignItems: "center",
25+
gap: 2,
26+
},
27+
plusButtonContainerColumn: {
28+
flexDirection: "column",
29+
alignItems: "stretch",
2530
},
2631
footer: {
2732
lineHeight: 1.7,
@@ -33,19 +38,22 @@ const styles: SXStyles = {
3338
export default function AccountsList({
3439
accounts,
3540
onEditAccount,
41+
onUseAnyAccount,
3642
createdAccountAddress,
3743
flowAccountAddress,
3844
flowAccountPrivateKey,
3945
avatarUrl,
4046
}: {
4147
accounts: Account[]
4248
onEditAccount: (account: Account | NewAccount) => void
49+
onUseAnyAccount: () => void
4350
createdAccountAddress: string | null
4451
flowAccountAddress: string
4552
flowAccountPrivateKey: string
4653
avatarUrl: string
4754
}) {
4855
const {initError} = useAuthnContext()
56+
const {forkMode} = useConfig()
4957

5058
return (
5159
<div>
@@ -78,7 +86,20 @@ export default function AccountsList({
7886
/>
7987
))}
8088
</Box>
81-
<Box sx={styles.plusButtonContainer}>
89+
<Box
90+
sx={{
91+
...styles.plusButtonContainer,
92+
...(forkMode ? styles.plusButtonContainerColumn : {}),
93+
}}
94+
>
95+
{forkMode && (
96+
<PlusButton
97+
onClick={onUseAnyAccount}
98+
data-test="use-any-account-button"
99+
>
100+
Use Existing Address (Fork Mode)
101+
</PlusButton>
102+
)}
82103
<PlusButton
83104
onClick={() =>
84105
onEditAccount(accountGenerator(accounts.length - 1))

‎components/AnyAccountForm.tsx‎

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

‎components/AuthzDetailsTable.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ const styles: SXStyles = {
6060
},
6161
}
6262

63-
export function AuthzDetailsAccount({account}: {account: Account}) {
63+
export function AuthzDetailsAccount({account}: {account: Account | null}) {
6464
const {currentUser} = useAuthzContext()
65-
const isCurrent = account.address === currentUser.address
65+
if (!account) return null
66+
const isCurrent = currentUser && account.address === currentUser.address
6667
return (
6768
<div sx={styles.accountDetail}>
6869
<div
@@ -71,7 +72,7 @@ export function AuthzDetailsAccount({account}: {account: Account}) {
7172
...(isCurrent ? styles.accountDetailLabelCurrent : {}),
7273
}}
7374
>
74-
{account.label}
75+
{account.label || account.address}
7576
</div>
7677
<div sx={styles.accountAddress}>{account.address}</div>
7778
</div>

‎components/AuthzHeader.tsx‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,23 @@ function AuthzHeader({
6060
<img src="/transaction.svg" />
6161
</div>
6262
<div sx={styles.headerSection}>
63-
<AccountImage
64-
address={currentUser.address}
65-
seed={title}
66-
lg={true}
67-
flowAccountAddress={flowAccountAddress}
68-
avatarUrl={avatarUrl}
69-
/>
70-
<Label sx={styles.label}>
71-
<div sx={styles.greenDot} />
72-
{currentUser.label}
73-
</Label>
63+
{currentUser && (
64+
<>
65+
<AccountImage
66+
address={currentUser.address}
67+
seed={title}
68+
lg={true}
69+
flowAccountAddress={flowAccountAddress}
70+
avatarUrl={avatarUrl}
71+
/>
72+
<Label sx={styles.label}>
73+
<div sx={styles.greenDot} />
74+
{currentUser.label !== currentUser.address
75+
? currentUser.label
76+
: `External (${currentUser.address.slice(0, 8)}...)`}
77+
</Label>
78+
</>
79+
)}
7480
</div>
7581
<div sx={styles.headerSection}>
7682
<ConnectedAppIcon icon={icon} />

0 commit comments

Comments
 (0)