Skip to content

Commit d4b9e65

Browse files
authored
Merge pull request #8 from citizenwallet/readonly-mode
read-only mode
2 parents c704c4b + 7254efe commit d4b9e65

File tree

10 files changed

+849
-25
lines changed

10 files changed

+849
-25
lines changed

package-lock.json

Lines changed: 491 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
"start": "next start",
1010
"lint": "next lint",
1111
"test": "jest",
12-
"test-watch": "jest --watch"
12+
"test-watch": "jest --watch",
13+
"enable-provider": "tsx scripts/enable-provider.ts"
1314
},
1415
"dependencies": {
15-
"@citizenwallet/sdk": "^2.0.71",
16+
"@citizenwallet/sdk": "^2.0.75",
1617
"@radix-ui/react-avatar": "^1.0.4",
1718
"@radix-ui/react-dialog": "^1.1.2",
1819
"@radix-ui/react-label": "^2.1.1",
@@ -56,6 +57,7 @@
5657
"postcss": "^8",
5758
"tailwindcss": "^3.4.1",
5859
"ts-jest": "^29.1.5",
60+
"tsx": "^4.19.3",
5961
"typescript": "^5"
6062
},
6163
"overrides": {

scripts/enable-provider.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
CommunityConfig,
3+
createInstanceCallData,
4+
instanceOwner,
5+
} from "@citizenwallet/sdk";
6+
import { Wallet, ZeroAddress } from "ethers";
7+
import { BundlerService, getAccountAddress } from "@citizenwallet/sdk";
8+
import { readCommunityFile } from "@/services/config";
9+
10+
interface CommunityWithContracts {
11+
community: CommunityConfig;
12+
contracts: string[];
13+
}
14+
15+
const main = async () => {
16+
const alias = process.argv[2];
17+
console.log("alias", alias);
18+
const config = await readCommunityFile(alias);
19+
if (!config) {
20+
throw new Error("No communities found");
21+
}
22+
23+
const community = new CommunityConfig(config);
24+
25+
console.log("community", community);
26+
27+
const privateKey = process.env.SERVER_PRIVATE_KEY;
28+
if (!privateKey) {
29+
throw new Error("Private key is not set");
30+
}
31+
32+
const signer = new Wallet(privateKey);
33+
34+
const bundler = new BundlerService(community);
35+
const cardConfig = community.primarySafeCardConfig;
36+
37+
console.log("cardConfig", cardConfig);
38+
39+
const owner = await instanceOwner(community);
40+
if (owner === ZeroAddress) {
41+
const ccalldata = createInstanceCallData(community, [
42+
community.primaryToken.address,
43+
community.community.profile.address,
44+
]);
45+
46+
const signerAccountAddress = await getAccountAddress(
47+
community,
48+
signer.address
49+
);
50+
if (!signerAccountAddress) {
51+
throw new Error("Could not find an account for you!");
52+
}
53+
54+
const hash = await bundler.call(
55+
signer,
56+
cardConfig.address,
57+
signerAccountAddress,
58+
ccalldata
59+
);
60+
61+
console.log("submitted:", hash);
62+
63+
await bundler.awaitSuccess(hash);
64+
65+
console.log("Instance created");
66+
} else {
67+
console.log("Instance already exists");
68+
}
69+
};
70+
71+
main();

src/app/[address]/page.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { headers } from "next/headers";
2+
import { getCommunityFromHeaders } from "@/services/config";
3+
import Wallet from "@/containers/wallet";
4+
import ReadOnly from "./readonly";
5+
6+
interface PageProps {
7+
params: Promise<{
8+
address: string;
9+
}>;
10+
}
11+
12+
export default async function Page(props: PageProps) {
13+
14+
const headersList = await headers();
15+
16+
const config = await getCommunityFromHeaders(headersList);
17+
if (!config) {
18+
return <div>Community not found</div>;
19+
}
20+
21+
22+
const { address } = await props.params;
23+
24+
return (
25+
<>
26+
<ReadOnly config={config} accountAddress={address} />
27+
</>
28+
)
29+
}

src/app/[address]/readonly.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use client";
2+
3+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4+
import { useToast } from "@/components/ui/use-toast";
5+
import WalletAction from "@/components/wallet/Action";
6+
import ActionBar from "@/components/wallet/ActionBar";
7+
import TxRow from "@/components/wallet/TxRow";
8+
import ReceiveModal from "@/containers/wallet/ReceiveModal";
9+
import { useHash } from "@/hooks/hash";
10+
import { useIsScrolled } from "@/hooks/scroll";
11+
import { useThemeUpdater } from "@/hooks/theme";
12+
import { useFocusEffect } from "@/hooks/useFocusEffect";
13+
import { useScrollableWindowFetcher } from "@/hooks/useScrollableWindow";
14+
import { cn, getAvatarUrl } from "@/lib/utils";
15+
import WalletKitService from "@/services/walletkit";
16+
import { AccountLogic, useAccount } from "@/state/account/actions";
17+
import { selectOrderedLogs } from "@/state/account/selectors";
18+
import { useProfiles } from "@/state/profiles/actions";
19+
import { useSend } from "@/state/send/actions";
20+
import { useVoucher } from "@/state/voucher/actions";
21+
import { getBaseUrl, getFullUrl } from "@/utils/deeplink";
22+
import { getWindow } from "@/utils/window";
23+
import {
24+
CommunityConfig,
25+
Config,
26+
QRFormat,
27+
getAccountBalance,
28+
parseQRFormat,
29+
} from "@citizenwallet/sdk";
30+
import { Box, Flex, Text } from "@radix-ui/themes";
31+
import { ArrowDownIcon } from "lucide-react";
32+
import Link from "next/link";
33+
import { useCallback, useEffect } from "react";
34+
interface ContainerProps {
35+
config: Config;
36+
accountAddress: string;
37+
}
38+
39+
export default function ReadOnly({ config, accountAddress }: ContainerProps) {
40+
const { community } = config;
41+
42+
const communityConfig = new CommunityConfig(config);
43+
44+
const isScrolled = useIsScrolled();
45+
46+
const baseUrl = getBaseUrl();
47+
48+
const [state, actions] = useAccount(baseUrl, config);
49+
const [_, sendActions] = useSend();
50+
const [profilesState, profilesActions] = useProfiles(config);
51+
const [voucherState, voucherActions] = useVoucher(config);
52+
const hash = useHash();
53+
54+
const { toast } = useToast();
55+
56+
useThemeUpdater(community);
57+
58+
useEffect(() => {
59+
actions.getAccount(accountAddress);
60+
}, [accountAddress, actions]);
61+
62+
const account = state((state) => state.account);
63+
64+
useFocusEffect(() => {
65+
let unsubscribe: () => void | undefined;
66+
67+
if (account) {
68+
profilesActions.loadProfile(account);
69+
actions.fetchBalance();
70+
unsubscribe = actions.listen(account);
71+
}
72+
73+
return () => {
74+
unsubscribe?.();
75+
};
76+
}, [account]);
77+
78+
const fetchMoreTransfers = useCallback(async () => {
79+
if (!account) return false;
80+
return actions.getTransfers(account);
81+
}, [actions, account]);
82+
83+
const scrollableRef = useScrollableWindowFetcher(fetchMoreTransfers);
84+
85+
const balance = state((state) => state.balance);
86+
const logs = state(selectOrderedLogs);
87+
const profile = profilesState((state) => state.profiles[account]);
88+
const profiles = profilesState((state) => state.profiles);
89+
90+
return (
91+
<main
92+
ref={scrollableRef}
93+
className="relative flex min-h-screen w-full flex-col align-center p-4 max-w-xl"
94+
>
95+
<Link
96+
href={`/profile/${account}`}
97+
className="z-20 absolute right-0 top-0"
98+
>
99+
<Avatar className="h-11 w-11 m-4 border-2 border-primary">
100+
<AvatarImage
101+
src={getAvatarUrl(profile?.image_small, account)}
102+
alt="profile image"
103+
/>
104+
<AvatarFallback>{!profile ? "PRF" : profile.username}</AvatarFallback>
105+
</Avatar>
106+
</Link>
107+
108+
<ActionBar
109+
readonly
110+
account={account}
111+
balance={balance}
112+
config={config}
113+
accountActions={actions}
114+
/>
115+
116+
<Flex direction="column" className="w-full pt-[420px]" gap="3">
117+
{logs.map((tx) => (
118+
<TxRow
119+
key={tx.hash}
120+
token={communityConfig.primaryToken}
121+
community={community}
122+
account={account}
123+
tx={tx}
124+
actions={profilesActions}
125+
profiles={profiles}
126+
/>
127+
))}
128+
</Flex>
129+
</main>
130+
);
131+
}

src/components/wallet/ActionBar.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
1010
import PluginsSheet from "./PluginsSheet";
1111

1212
interface ActionBarProps {
13+
readonly?: boolean;
1314
account: string;
1415
balance: string;
1516
small?: boolean;
@@ -18,6 +19,7 @@ interface ActionBarProps {
1819
}
1920

2021
export default function ActionBar({
22+
readonly = false,
2123
account,
2224
balance,
2325
small,
@@ -104,13 +106,15 @@ export default function ActionBar({
104106
small ? "pt-2 pb-4 pr-4" : "pt-4 pr-4"
105107
)}
106108
>
107-
<SendModal config={config} accountActions={accountActions}>
108-
<WalletAction
109-
compact={small}
110-
icon={<ArrowUpIcon size={small ? 30 : 40} />}
111-
label="Send"
112-
/>
113-
</SendModal>
109+
{!readonly && (
110+
<SendModal config={config} accountActions={accountActions}>
111+
<WalletAction
112+
compact={small}
113+
icon={<ArrowUpIcon size={small ? 30 : 40} />}
114+
label="Send"
115+
/>
116+
</SendModal>
117+
)}
114118

115119
<ReceiveModal token={primaryToken} communityConfig={communityConfig}>
116120
<WalletAction

src/services/account/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export class CWAccount {
1818
config: Config;
1919
communityConfig: CommunityConfig;
2020
account: string;
21-
signer: Wallet | HDNodeWallet;
21+
signer?: Wallet | HDNodeWallet;
2222

23-
constructor(config: Config, account: string, signer: Wallet | HDNodeWallet) {
23+
constructor(config: Config, account: string, signer?: Wallet | HDNodeWallet) {
2424
this.communityConfig = new CommunityConfig(config);
2525

2626
this.provider = new JsonRpcProvider(this.communityConfig.primaryRPCUrl);
@@ -107,6 +107,10 @@ export class CWAccount {
107107
async send(to: string, amount: string, description?: string) {
108108
const primaryToken = this.communityConfig.primaryToken;
109109

110+
if (!this.signer) {
111+
throw new Error("Signer not found");
112+
}
113+
110114
const hash = await this.bundler.sendERC20Token(
111115
this.signer,
112116
primaryToken.address,
@@ -125,6 +129,10 @@ export class CWAccount {
125129
? new Uint8Array(Buffer.from(data.slice(2), "hex"))
126130
: new Uint8Array(Buffer.from(data, "hex"));
127131

132+
if (!this.signer) {
133+
throw new Error("Signer not found");
134+
}
135+
128136
const hash = await this.bundler.call(
129137
this.signer,
130138
to,

src/services/config/communities.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@
8989
"chain_id": 100
9090
},
9191
"primary_account_factory": {
92-
"address": "0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE",
92+
"address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2",
9393
"chain_id": 100
9494
},
9595
"primary_card_manager": {
96-
"address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB",
96+
"address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28",
9797
"chain_id": 100
9898
}
9999
},
@@ -122,7 +122,7 @@
122122
"100:0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2": {
123123
"chain_id": 100,
124124
"entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6",
125-
"paymaster_address": "0x8fc2e97671C691e7Ff7B42e5c7cCbDD38fC8B729",
125+
"paymaster_address": "0xE69C843898E21C0E95eA7DD310cD850AAc0aB897",
126126
"account_factory_address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2",
127127
"paymaster_type": "cw-safe"
128128
}
@@ -132,6 +132,12 @@
132132
"chain_id": 100,
133133
"address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB",
134134
"type": "classic"
135+
},
136+
"100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": {
137+
"chain_id": 100,
138+
"instance_id": "brussels-pay",
139+
"address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28",
140+
"type": "safe"
135141
}
136142
},
137143
"chains": {

0 commit comments

Comments
 (0)