From 7fee5b62b3107a3b37865369d893cf56742219e7 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Thu, 31 Oct 2024 21:16:57 -0400 Subject: [PATCH 01/11] Support chain switching --- examples/next/src/app/page.tsx | 6 +- examples/next/src/components/Header.tsx | 135 ++++++++++++++++++ .../components/providers/StarknetProvider.tsx | 2 +- packages/controller/src/account.ts | 3 +- packages/controller/src/controller.ts | 69 +++++++-- packages/controller/src/provider.ts | 34 ++--- packages/controller/src/session/provider.ts | 18 ++- packages/controller/src/telegram/provider.ts | 19 ++- packages/controller/src/types.ts | 13 +- 9 files changed, 254 insertions(+), 45 deletions(-) create mode 100644 examples/next/src/components/Header.tsx diff --git a/examples/next/src/app/page.tsx b/examples/next/src/app/page.tsx index 46f4984ec..adb934ec9 100644 --- a/examples/next/src/app/page.tsx +++ b/examples/next/src/app/page.tsx @@ -1,13 +1,12 @@ import { Transfer } from "components/Transfer"; import { ManualTransferEth } from "components/ManualTransferEth"; -import { ConnectWallet } from "components/ConnectWallet"; import { InvalidTxn } from "components/InvalidTxn"; import { SignMessage } from "components/SignMessage"; import { DelegateAccount } from "components/DelegateAccount"; import { ColorModeToggle } from "components/ColorModeToggle"; import { Profile } from "components/Profile"; -import { Settings } from "components/Settings"; import { LookupControllers } from "components/LookupControllers"; +import Header from "components/Header"; export default function Home() { return ( @@ -18,8 +17,7 @@ export default function Home() { - - +
diff --git a/examples/next/src/components/Header.tsx b/examples/next/src/components/Header.tsx new file mode 100644 index 000000000..76cf50999 --- /dev/null +++ b/examples/next/src/components/Header.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { Button } from "@cartridge/ui-next"; +import ControllerConnector from "@cartridge/connector/controller"; +import { useAccount, useConnect, useDisconnect } from "@starknet-react/core"; +import { useState } from "react"; + +const Header = ({ + showBack, + lockChain, +}: { + showBack?: boolean; + lockChain?: boolean; +}) => { + const { connect, connectors } = useConnect(); + const { disconnect } = useDisconnect(); + const { address, connector } = useAccount(); + const controllerConnector = connector as never as ControllerConnector; + const chains = [ + { name: "Mainnet", id: "mainnet" }, + { name: "Sepolia (Testnet)", id: "sepolia" }, + { name: "Slot (L3)", id: "slot" }, + ]; + + const chainName = "chain name"; + const [networkOpen, setNetworkOpen] = useState(false); + const [profileOpen, setProfileOpen] = useState(false); + + // const chainName = useMemo(() => { + // return chains.find((c) => c.id === getCurrentChain())?.name; + // }, [chains, getCurrentChain]); + + return ( +
+ {showBack && ( + + )} +
+
+ + {networkOpen && ( +
+ {chains.map((c) => ( + + ))} +
+ )} +
+ {address ? ( +
+ + {profileOpen && ( +
+ + + +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default Header; diff --git a/examples/next/src/components/providers/StarknetProvider.tsx b/examples/next/src/components/providers/StarknetProvider.tsx index b0fad14af..f05ccd0c8 100644 --- a/examples/next/src/components/providers/StarknetProvider.tsx +++ b/examples/next/src/components/providers/StarknetProvider.tsx @@ -121,7 +121,7 @@ export function StarknetProvider({ children }: PropsWithChildren) { const controller = new ControllerConnector({ policies, - rpc, + rpcUrl: rpc, url: process.env.NEXT_PUBLIC_KEYCHAIN_DEPLOYMENT_URL ?? process.env.NEXT_PUBLIC_KEYCHAIN_FRAME_URL, diff --git a/packages/controller/src/account.ts b/packages/controller/src/account.ts index 13499d8e4..a575f4c6d 100644 --- a/packages/controller/src/account.ts +++ b/packages/controller/src/account.ts @@ -27,12 +27,13 @@ class ControllerAccount extends WalletAccount { constructor( provider: BaseProvider, + rpcUrl: string, address: string, keychain: AsyncMethodReturns, options: KeychainOptions, modal: Modal, ) { - super({ nodeUrl: provider.rpc.toString() }, provider); + super({ nodeUrl: rpcUrl }, provider); this.address = address; this.keychain = keychain; diff --git a/packages/controller/src/controller.ts b/packages/controller/src/controller.ts index 847259602..7b46a249e 100644 --- a/packages/controller/src/controller.ts +++ b/packages/controller/src/controller.ts @@ -13,20 +13,61 @@ import { Profile, IFrames, ProfileContextTypeVariant, + Chain, } from "./types"; import BaseProvider from "./provider"; -import { WalletAccount } from "starknet"; +import { constants, WalletAccount } from "starknet"; import { Policy } from "@cartridge/presets"; +import { AddStarknetChainParameters, ChainId } from "@starknet-io/types-js"; export default class ControllerProvider extends BaseProvider { private keychain?: AsyncMethodReturns; private profile?: AsyncMethodReturns; private options: ControllerOptions; private iframes: IFrames; + private selectedChain: ChainId; + private chains: Map; constructor(options: ControllerOptions) { - const { rpc } = options; - super({ rpc }); + super(); + + const chains = new Map(); + + for (const chain of options.chains) { + let chainId: ChainId | undefined; + const url = new URL(chain.rpcUrl); + const parts = url.pathname.split("/"); + + if (parts.includes("starknet")) { + if (parts.includes("mainnet")) { + chainId = constants.StarknetChainId.SN_MAIN; + } else if (parts.includes("sepolia")) { + chainId = constants.StarknetChainId.SN_SEPOLIA; + } + } else if (parts.length >= 3) { + const projectName = parts[2]; + if (parts.includes("katana")) { + chainId = `WP_${projectName.toUpperCase()}` as ChainId; + } else if (parts.includes("mainnet")) { + chainId = `GG_${projectName.toUpperCase()}` as ChainId; + } + } + + if (!chainId) { + throw new Error(`Chain ${chain.rpcUrl} not supported`); + } + + chains.set(chainId, chain); + } + + this.chains = chains; + this.selectedChain = options.defaultChainId; + + if (!this.chains.has(this.selectedChain)) { + throw new Error( + `Chain ${this.selectedChain} not found in configured chains`, + ); + } this.iframes = { keychain: new KeychainIFrame({ @@ -54,12 +95,11 @@ export default class ControllerProvider extends BaseProvider { return; } - const response = (await this.keychain.probe( - this.rpc.toString(), - )) as ProbeReply; + const response = (await this.keychain.probe(this.rpcUrl())) as ProbeReply; this.account = new ControllerAccount( this, + this.rpcUrl(), response.address, this.keychain, this.options, @@ -83,7 +123,7 @@ export default class ControllerProvider extends BaseProvider { openPurchaseCredits: () => this.openPurchaseCredits.bind(this), openExecute: () => this.openExecute.bind(this), }, - rpcUrl: this.rpc.toString(), + rpcUrl: this.rpcUrl(), username, version: this.version, }); @@ -114,7 +154,7 @@ export default class ControllerProvider extends BaseProvider { try { let response = await this.keychain.connect( this.options.policies || [], - this.rpc.toString(), + this.rpcUrl(), ); if (response.code !== ResponseCodes.SUCCESS) { throw new Error(response.message); @@ -123,6 +163,7 @@ export default class ControllerProvider extends BaseProvider { response = response as ConnectReply; this.account = new ControllerAccount( this, + this.rpcUrl(), response.address, this.keychain, this.options, @@ -137,6 +178,14 @@ export default class ControllerProvider extends BaseProvider { } } + switchStarknetChain(_chainId: string): Promise { + return Promise.resolve(true); + } + + addStarknetChain(_chain: AddStarknetChainParameters): Promise { + return Promise.resolve(true); + } + async disconnect() { if (!this.keychain) { console.error(new NotReadyToConnect().message); @@ -197,6 +246,10 @@ export default class ControllerProvider extends BaseProvider { return this.keychain.revoke(origin); } + rpcUrl(): string { + return this.chains.get(this.selectedChain)!.rpcUrl; + } + username() { if (!this.keychain) { console.error(new NotReadyToConnect().message); diff --git a/packages/controller/src/provider.ts b/packages/controller/src/provider.ts index bc32df4d3..8b807f791 100644 --- a/packages/controller/src/provider.ts +++ b/packages/controller/src/provider.ts @@ -1,10 +1,12 @@ import { WalletAccount } from "starknet"; import { AddInvokeTransactionParameters, + AddStarknetChainParameters, Errors, Permission, RequestFn, StarknetWindowObject, + SwitchStarknetChainParameters, TypedData, WalletEventHandlers, WalletEventListener, @@ -13,7 +15,6 @@ import { import manifest from "../package.json"; import { icon } from "./icon"; -import { ProviderOptions } from "./types"; export default abstract class BaseProvider implements StarknetWindowObject { public id = "controller"; @@ -21,16 +22,9 @@ export default abstract class BaseProvider implements StarknetWindowObject { public version = manifest.version; public icon = icon; - public rpc: URL; public account?: WalletAccount; public subscriptions: WalletEvents[] = []; - constructor(options: ProviderOptions) { - const { rpc } = options; - - this.rpc = new URL(rpc); - } - request: RequestFn = async (call) => { switch (call.type) { case "wallet_getPermissions": @@ -66,19 +60,15 @@ export default abstract class BaseProvider implements StarknetWindowObject { data: "wallet_watchAsset not implemented", } as Errors.UNEXPECTED_ERROR; - case "wallet_addStarknetChain": - throw { - code: 63, - message: "An unexpected error occurred", - data: "wallet_addStarknetChain not implemented", - } as Errors.UNEXPECTED_ERROR; + case "wallet_addStarknetChain": { + let params = call.params as AddStarknetChainParameters; + return this.addStarknetChain(params); + } - case "wallet_switchStarknetChain": - throw { - code: 63, - message: "An unexpected error occurred", - data: "wallet_switchStarknetChain not implemented", - } as Errors.UNEXPECTED_ERROR; + case "wallet_switchStarknetChain": { + let params = call.params as SwitchStarknetChainParameters; + return this.switchStarknetChain(params.chainId); + } case "wallet_requestChainId": if (!this.account) { @@ -175,4 +165,8 @@ export default abstract class BaseProvider implements StarknetWindowObject { abstract probe(): Promise; abstract connect(): Promise; + abstract switchStarknetChain(chainId: string): Promise; + abstract addStarknetChain( + chain: AddStarknetChainParameters, + ): Promise; } diff --git a/packages/controller/src/session/provider.ts b/packages/controller/src/session/provider.ts index 5611227ba..443798bc3 100644 --- a/packages/controller/src/session/provider.ts +++ b/packages/controller/src/session/provider.ts @@ -5,6 +5,7 @@ import { KEYCHAIN_URL } from "../constants"; import BaseProvider from "../provider"; import { toWasmPolicies } from "../utils"; import { SessionPolicies } from "@cartridge/presets"; +import { AddStarknetChainParameters } from "@starknet-io/types-js"; interface SessionRegistration { username: string; @@ -26,14 +27,15 @@ export default class SessionProvider extends BaseProvider { public name = "Controller Session"; protected _chainId: string; - + protected _rpcUrl: string; protected _username?: string; protected _redirectUrl: string; protected _policies: SessionPolicies; constructor({ rpc, chainId, policies, redirectUrl }: SessionOptions) { - super({ rpc }); + super(); + this._rpcUrl = rpc; this._chainId = chainId; this._redirectUrl = redirectUrl; this._policies = policies; @@ -76,7 +78,7 @@ export default class SessionProvider extends BaseProvider { this._redirectUrl }&redirect_query_name=startapp&policies=${JSON.stringify( this._policies, - )}&rpc_url=${this.rpc}`; + )}&rpc_url=${this._rpcUrl}`; localStorage.setItem("lastUsedConnector", this.id); window.open(url, "_blank"); @@ -84,6 +86,14 @@ export default class SessionProvider extends BaseProvider { return; } + switchStarknetChain(_chainId: string): Promise { + throw new Error("switchStarknetChain not implemented"); + } + + addStarknetChain(_chain: AddStarknetChainParameters): Promise { + throw new Error("addStarknetChain not implemented"); + } + disconnect(): Promise { localStorage.removeItem("sessionSigner"); localStorage.removeItem("session"); @@ -127,7 +137,7 @@ export default class SessionProvider extends BaseProvider { this._username = sessionRegistration.username; this.account = new SessionAccount(this, { - rpcUrl: this.rpc.toString(), + rpcUrl: this._rpcUrl, privateKey: signer.privKey, address: sessionRegistration.address, ownerGuid: sessionRegistration.ownerGuid, diff --git a/packages/controller/src/telegram/provider.ts b/packages/controller/src/telegram/provider.ts index 83cc3cbc2..89d33b417 100644 --- a/packages/controller/src/telegram/provider.ts +++ b/packages/controller/src/telegram/provider.ts @@ -11,6 +11,7 @@ import SessionAccount from "../session/account"; import BaseProvider from "../provider"; import { toWasmPolicies } from "../utils"; import { SessionPolicies } from "@cartridge/presets"; +import { AddStarknetChainParameters } from "@starknet-io/types-js"; interface SessionRegistration { username: string; @@ -25,6 +26,7 @@ export default class TelegramProvider extends BaseProvider { protected _chainId: string; protected _username?: string; protected _policies: SessionPolicies; + private _rpcUrl: string; constructor({ rpc, @@ -37,10 +39,9 @@ export default class TelegramProvider extends BaseProvider { policies: SessionPolicies; tmaUrl: string; }) { - super({ - rpc, - }); + super(); + this._rpcUrl = rpc; this._tmaUrl = tmaUrl; this._chainId = chainId; this._policies = policies; @@ -77,7 +78,7 @@ export default class TelegramProvider extends BaseProvider { this._tmaUrl }&redirect_query_name=startapp&policies=${JSON.stringify( this._policies, - )}&rpc_url=${this.rpc}`; + )}&rpc_url=${this._rpcUrl}`; localStorage.setItem("lastUsedConnector", this.id); openLink(url); @@ -86,6 +87,14 @@ export default class TelegramProvider extends BaseProvider { return; } + switchStarknetChain(_chainId: string): Promise { + throw new Error("switchStarknetChain not implemented"); + } + + addStarknetChain(_chain: AddStarknetChainParameters): Promise { + throw new Error("addStarknetChain not implemented"); + } + disconnect(): Promise { cloudStorage.deleteItem("sessionSigner"); cloudStorage.deleteItem("session"); @@ -118,7 +127,7 @@ export default class TelegramProvider extends BaseProvider { this._username = sessionRegistration.username; this.account = new SessionAccount(this, { - rpcUrl: this.rpc.toString(), + rpcUrl: this._rpcUrl, privateKey: signer.privKey, address: sessionRegistration.address, ownerGuid: sessionRegistration.ownerGuid, diff --git a/packages/controller/src/types.ts b/packages/controller/src/types.ts index 73170283c..cd8957c5e 100644 --- a/packages/controller/src/types.ts +++ b/packages/controller/src/types.ts @@ -7,6 +7,7 @@ import { } from "starknet"; import { AddInvokeTransactionResult, + ChainId, Signature, TypedData, } from "@starknet-io/types-js"; @@ -17,6 +18,7 @@ import { Policy, SessionPolicies, } from "@cartridge/presets"; +import { RequestFn } from "@starknet-io/types-js"; export type Session = { chainId: constants.StarknetChainId; @@ -133,7 +135,10 @@ export interface Keychain { fetchControllers(contractAddresses: string[]): Promise; openPurchaseCredits(): void; openExecute(calls: Call[]): Promise; + + request: RequestFn; } + export interface Profile { navigate(path: string): void; } @@ -161,9 +166,13 @@ export type IFrameOptions = { colorMode?: ColorMode; }; +export type Chain = { + rpcUrl: string; +}; + export type ProviderOptions = { - /** The URL of the RPC */ - rpc: string; + defaultChainId: ChainId; + chains: Chain[]; }; export type KeychainOptions = IFrameOptions & { From 827d2b3eddafff02ba007405ff8428a742404bcd Mon Sep 17 00:00:00 2001 From: broody Date: Mon, 6 Jan 2025 14:08:32 -1000 Subject: [PATCH 02/11] Add chain-id to account storage key --- packages/account_sdk/src/controller.rs | 1 + packages/account_sdk/src/storage/mod.rs | 14 +++++++++++--- packages/account_sdk/src/storage/selectors.rs | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/account_sdk/src/controller.rs b/packages/account_sdk/src/controller.rs index c742f8e5b..8647b40e0 100644 --- a/packages/account_sdk/src/controller.rs +++ b/packages/account_sdk/src/controller.rs @@ -99,6 +99,7 @@ impl Controller { .storage .set_controller( app_id.as_str(), + &chain_id, address, ControllerMetadata::from(&controller), ) diff --git a/packages/account_sdk/src/storage/mod.rs b/packages/account_sdk/src/storage/mod.rs index 78d5f6677..3ae57de49 100644 --- a/packages/account_sdk/src/storage/mod.rs +++ b/packages/account_sdk/src/storage/mod.rs @@ -223,6 +223,7 @@ pub struct Credentials { #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct ActiveMetadata { address: Felt, + chain_id: Felt, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -256,7 +257,10 @@ pub trait StorageBackend: Send + Sync { self.get(&selectors::Selectors::active(app_id)) .and_then(|value| match value { Some(StorageValue::Active(metadata)) => self - .get(&selectors::Selectors::account(&metadata.address)) + .get(&selectors::Selectors::account( + &metadata.address, + &metadata.chain_id, + )) .and_then(|value| match value { Some(StorageValue::Controller(metadata)) => Ok(Some(metadata)), Some(_) => Err(StorageError::TypeMismatch), @@ -270,15 +274,19 @@ pub trait StorageBackend: Send + Sync { fn set_controller( &mut self, app_id: &str, + chain_id: &Felt, address: Felt, metadata: ControllerMetadata, ) -> Result<(), StorageError> { self.set( &selectors::Selectors::active(app_id), - &StorageValue::Active(ActiveMetadata { address }), + &StorageValue::Active(ActiveMetadata { + address, + chain_id: *chain_id, + }), )?; self.set( - &selectors::Selectors::account(&address), + &selectors::Selectors::account(&address, chain_id), &StorageValue::Controller(metadata), ) } diff --git a/packages/account_sdk/src/storage/selectors.rs b/packages/account_sdk/src/storage/selectors.rs index 2808942b1..40d1f46cc 100644 --- a/packages/account_sdk/src/storage/selectors.rs +++ b/packages/account_sdk/src/storage/selectors.rs @@ -7,8 +7,8 @@ impl Selectors { format!("@cartridge/{}/active", app_id) } - pub fn account(address: &Felt) -> String { - format!("@cartridge/account/0x{:x}", address) + pub fn account(address: &Felt, chain_id: &Felt) -> String { + format!("@cartridge/account/0x{:x}/0x{:x}", address, chain_id) } pub fn deployment(address: &Felt, chain_id: &Felt) -> String { From 9da6f9addbf6044b6ac2ee3be1c9885c49dc12bc Mon Sep 17 00:00:00 2001 From: broody Date: Tue, 7 Jan 2025 09:30:38 -1000 Subject: [PATCH 03/11] Fix example with multi chains --- .../next/src/components/providers/StarknetProvider.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/next/src/components/providers/StarknetProvider.tsx b/examples/next/src/components/providers/StarknetProvider.tsx index f05ccd0c8..7c04d4a23 100644 --- a/examples/next/src/components/providers/StarknetProvider.tsx +++ b/examples/next/src/components/providers/StarknetProvider.tsx @@ -9,8 +9,7 @@ import { import { PropsWithChildren } from "react"; import ControllerConnector from "@cartridge/connector/controller"; import { Policy } from "@cartridge/controller"; - -const rpc = process.env.NEXT_PUBLIC_RPC_SEPOLIA!; +import { constants } from "starknet"; export const ETH_CONTRACT_ADDRESS = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; @@ -121,7 +120,11 @@ export function StarknetProvider({ children }: PropsWithChildren) { const controller = new ControllerConnector({ policies, - rpcUrl: rpc, + chains: [ + { rpcUrl: process.env.NEXT_PUBLIC_RPC_SEPOLIA! }, + { rpcUrl: process.env.NEXT_PUBLIC_RPC_MAINNET! }, + ], + defaultChainId: constants.StarknetChainId.SN_SEPOLIA, url: process.env.NEXT_PUBLIC_KEYCHAIN_DEPLOYMENT_URL ?? process.env.NEXT_PUBLIC_KEYCHAIN_FRAME_URL, From 219db54da09c1cbc42892423496a6c779a6a5a03 Mon Sep 17 00:00:00 2001 From: broody Date: Tue, 7 Jan 2025 09:42:04 -1000 Subject: [PATCH 04/11] Decode network id --- examples/next/src/components/Header.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/next/src/components/Header.tsx b/examples/next/src/components/Header.tsx index 76cf50999..b5789adde 100644 --- a/examples/next/src/components/Header.tsx +++ b/examples/next/src/components/Header.tsx @@ -3,7 +3,8 @@ import { Button } from "@cartridge/ui-next"; import ControllerConnector from "@cartridge/connector/controller"; import { useAccount, useConnect, useDisconnect } from "@starknet-react/core"; -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { constants, num } from "starknet"; const Header = ({ showBack, @@ -14,21 +15,21 @@ const Header = ({ }) => { const { connect, connectors } = useConnect(); const { disconnect } = useDisconnect(); - const { address, connector } = useAccount(); + const { address, connector, chainId } = useAccount(); const controllerConnector = connector as never as ControllerConnector; const chains = [ - { name: "Mainnet", id: "mainnet" }, - { name: "Sepolia (Testnet)", id: "sepolia" }, - { name: "Slot (L3)", id: "slot" }, + { name: "Mainnet", id: constants.StarknetChainId.SN_MAIN }, + { name: "Sepolia", id: constants.StarknetChainId.SN_SEPOLIA }, ]; - const chainName = "chain name"; const [networkOpen, setNetworkOpen] = useState(false); const [profileOpen, setProfileOpen] = useState(false); - // const chainName = useMemo(() => { - // return chains.find((c) => c.id === getCurrentChain())?.name; - // }, [chains, getCurrentChain]); + const chainName = useMemo(() => { + if (chainId) { + return chains.find((c) => c.id === num.toHex(chainId))?.name; + } + }, [chains, chainId]); return (
@@ -43,7 +44,7 @@ const Header = ({ )}
-
+ {chainId &&
)} -
+
} {address ? (
)}
- {chainId &&
- - {networkOpen && ( -
- {chains.map((c) => ( - - ))} -
- )} -
} + {chainName} + + ▼ + + + {networkOpen && ( +
+ {chains.map((c) => ( + + ))} +
+ )} +
+ )} {address ? (
)}
- {chainId && ( + {status === "connected" && (
))}
From 2f22063adb7792ab5042a1c61a580fe302bd4885 Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 9 Jan 2025 08:15:28 -1000 Subject: [PATCH 10/11] update ui --- examples/next/src/components/Header.tsx | 53 +++++++++++-------- .../next/src/components/ManualTransferEth.tsx | 28 +++++----- examples/next/src/components/Transfer.tsx | 27 +++++----- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/examples/next/src/components/Header.tsx b/examples/next/src/components/Header.tsx index 939405b06..b0f4353a9 100644 --- a/examples/next/src/components/Header.tsx +++ b/examples/next/src/components/Header.tsx @@ -9,17 +9,11 @@ import { useNetwork, useSwitchChain, } from "@starknet-react/core"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { constants, num } from "starknet"; import { Chain } from "@starknet-react/chains"; -const Header = ({ - showBack, - lockChain, -}: { - showBack?: boolean; - lockChain?: boolean; -}) => { +const Header = () => { const { connect, connectors } = useConnect(); const { disconnect } = useDisconnect(); const { chain, chains } = useNetwork(); @@ -32,28 +26,41 @@ const Header = ({ chainId: constants.StarknetChainId.SN_SEPOLIA, }, }); + const networkRef = useRef(null); + const profileRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + networkRef.current && + !networkRef.current.contains(event.target as Node) + ) { + setNetworkOpen(false); + } + if ( + profileRef.current && + !profileRef.current.contains(event.target as Node) + ) { + setProfileOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); return (
- {showBack && ( - - )}
{status === "connected" && ( -
+
{networkOpen && ( -
+
{chains.map((c: Chain) => ( {profileOpen && ( -
+
@@ -60,16 +63,11 @@ export const ManualTransferEth = () => { Transfer 500 ETH to self {txnHash && ( -

- Transaction hash:{" "} - - {txnHash} - -

+ <> +

Transaction: {txnHash}

+ +

Chain: {network}

+ )}
); diff --git a/examples/next/src/components/Transfer.tsx b/examples/next/src/components/Transfer.tsx index 5b2d485a4..91ee89637 100644 --- a/examples/next/src/components/Transfer.tsx +++ b/examples/next/src/components/Transfer.tsx @@ -1,15 +1,16 @@ "use client"; import { Button } from "@cartridge/ui-next"; -import { useAccount, useExplorer } from "@starknet-react/core"; +import { useAccount, useNetwork } from "@starknet-react/core"; import { useCallback, useState } from "react"; import { STRK_CONTRACT_ADDRESS } from "./providers/StarknetProvider"; export const Transfer = () => { const [submitted, setSubmitted] = useState(false); const { account } = useAccount(); - const explorer = useExplorer(); + const { chain } = useNetwork(); const [txnHash, setTxnHash] = useState(); + const [network, setNetwork] = useState(); const execute = useCallback( async (amount: string) => { @@ -32,11 +33,14 @@ export const Transfer = () => { calldata: [account?.address, amount, "0x0"], }, ]) - .then(({ transaction_hash }) => setTxnHash(transaction_hash)) + .then(({ transaction_hash }) => { + setTxnHash(transaction_hash); + setNetwork(chain.network); + }) .catch((e) => console.error(e)) .finally(() => setSubmitted(false)); }, - [account], + [account, chain], ); if (!account) { @@ -46,7 +50,6 @@ export const Transfer = () => { return (

Session Transfer STRK

-

Address: {STRK_CONTRACT_ADDRESS}

{txnHash && ( -

- Transaction hash:{" "} - - {txnHash} - -

+ <> +

Transaction: {txnHash}

+

Chain: {network}

+ )}
); From 5918b44ca19d2cfb30041cb70f58549665d733d3 Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 9 Jan 2025 13:02:07 -1000 Subject: [PATCH 11/11] Message policy support support chain switching --- .../components/providers/StarknetProvider.tsx | 57 ++++++----- packages/controller/src/controller.ts | 12 ++- .../src/components/connect/CreateSession.tsx | 57 +++++------ .../components/connect/RegisterSession.tsx | 11 ++- .../src/components/session/AggregateCard.tsx | 96 ++++++++++--------- .../session/UnverifiedSessionSummary.tsx | 14 +-- .../session/VerifiedSessionSummary.tsx | 23 ++--- packages/keychain/src/hooks/session.ts | 32 ++++--- 8 files changed, 160 insertions(+), 142 deletions(-) diff --git a/examples/next/src/components/providers/StarknetProvider.tsx b/examples/next/src/components/providers/StarknetProvider.tsx index 4ab05792e..6c9e8a623 100644 --- a/examples/next/src/components/providers/StarknetProvider.tsx +++ b/examples/next/src/components/providers/StarknetProvider.tsx @@ -16,6 +16,35 @@ export const ETH_CONTRACT_ADDRESS = export const STRK_CONTRACT_ADDRESS = "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D"; +const messageForChain = (chainId: constants.StarknetChainId) => { + return { + types: { + StarknetDomain: [ + { name: "name", type: "shortstring" }, + { name: "version", type: "shortstring" }, + { name: "chainId", type: "shortstring" }, + { name: "revision", type: "shortstring" }, + ], + Person: [ + { name: "name", type: "felt" }, + { name: "wallet", type: "felt" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "felt" }, + ], + }, + primaryType: "Mail", + domain: { + name: "StarkNet Mail", + version: "1", + revision: "1", + chainId: chainId, + }, + }; +}; + const policies: SessionPolicies = { contracts: { [ETH_CONTRACT_ADDRESS]: { @@ -51,32 +80,8 @@ const policies: SessionPolicies = { }, }, messages: [ - // { - // types: { - // StarknetDomain: [ - // { name: "name", type: "shortstring" }, - // { name: "version", type: "shortstring" }, - // { name: "chainId", type: "shortstring" }, - // { name: "revision", type: "shortstring" }, - // ], - // Person: [ - // { name: "name", type: "felt" }, - // { name: "wallet", type: "felt" }, - // ], - // Mail: [ - // { name: "from", type: "Person" }, - // { name: "to", type: "Person" }, - // { name: "contents", type: "felt" }, - // ], - // }, - // primaryType: "Mail", - // domain: { - // name: "StarkNet Mail", - // version: "1", - // revision: "1", - // chainId: "SN_SEPOLIA", - // }, - // }, + messageForChain(constants.StarknetChainId.SN_MAIN), + messageForChain(constants.StarknetChainId.SN_SEPOLIA), ], }; diff --git a/packages/controller/src/controller.ts b/packages/controller/src/controller.ts index d6f8b0679..e162aea54 100644 --- a/packages/controller/src/controller.ts +++ b/packages/controller/src/controller.ts @@ -37,7 +37,6 @@ export default class ControllerProvider extends BaseProvider { let chainId: ChainId | undefined; const url = new URL(chain.rpcUrl); const parts = url.pathname.split("/"); - if (parts.includes("starknet")) { if (parts.includes("mainnet")) { chainId = constants.StarknetChainId.SN_MAIN; @@ -60,6 +59,17 @@ export default class ControllerProvider extends BaseProvider { chains.set(chainId, chain); } + if ( + options.policies?.messages?.length && + options.policies.messages.length !== chains.size + ) { + console.warn( + "Each message policy is associated with a specific chain. " + + "The number of message policies does not match the number of chains specified - " + + "session message signing may not work on some chains.", + ); + } + this.chains = chains; this.selectedChain = options.defaultChainId; diff --git a/packages/keychain/src/components/connect/CreateSession.tsx b/packages/keychain/src/components/connect/CreateSession.tsx index 865dd7101..8165cafb9 100644 --- a/packages/keychain/src/components/connect/CreateSession.tsx +++ b/packages/keychain/src/components/connect/CreateSession.tsx @@ -1,13 +1,11 @@ import { Container, Content, Footer } from "@/components/layout"; import { BigNumberish, shortString } from "starknet"; import { ControllerError } from "@/utils/connection"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useConnection } from "@/hooks/connection"; import { ControllerErrorAlert } from "@/components/ErrorAlert"; import { SessionConsent } from "@/components/connect"; import { Upgrade } from "./Upgrade"; -import { ErrorCode } from "@cartridge/account-wasm"; -import { TypedDataPolicy } from "@cartridge/presets"; import { ParsedSessionPolicies } from "@/hooks/session"; import { UnverifiedSessionSummary } from "@/components/session/UnverifiedSessionSummary"; import { VerifiedSessionSummary } from "@/components/session/VerifiedSessionSummary"; @@ -33,7 +31,6 @@ export function CreateSession({ }) { const { controller, upgrade, chainId, theme, logout } = useConnection(); const [isConnecting, setIsConnecting] = useState(false); - const [isDisabled, setIsDisabled] = useState(false); const [isConsent, setIsConsent] = useState(false); const [duration, setDuration] = useState(DEFAULT_SESSION_DURATION); const expiresAt = useMemo( @@ -43,32 +40,17 @@ export function CreateSession({ const [maxFee] = useState(); const [error, setError] = useState(); - useEffect(() => { - if (!chainId) return; - const normalizedChainId = normalizeChainId(chainId); - - const violatingPolicy = policies.messages?.find( - (policy: TypedDataPolicy) => - "domain" in policy && - (!policy.domain.chainId || - normalizeChainId(policy.domain.chainId) !== normalizedChainId), - ); - - if (violatingPolicy) { - setError({ - code: ErrorCode.PolicyChainIdMismatch, - message: `Policy for ${ - (violatingPolicy as TypedDataPolicy).domain.name - }.${ - (violatingPolicy as TypedDataPolicy).primaryType - } has mismatched chain ID.`, - }); - setIsDisabled(true); - } else { - setError(undefined); - setIsDisabled(false); - } - }, [chainId, policies]); + const chainSpecificMessages = useMemo(() => { + if (!policies.messages || !chainId) return []; + return policies.messages.filter((message) => { + return ( + !("domain" in message) || + (message.domain.chainId && + normalizeChainId(message.domain.chainId) === + normalizeChainId(chainId)) + ); + }); + }, [policies.messages, chainId]); const onCreateSession = useCallback(async () => { if (!controller || !policies) return; @@ -139,9 +121,16 @@ export function CreateSession({ {policies?.verified ? ( - + ) : ( - + )}