Skip to content

Commit

Permalink
Support chain switching
Browse files Browse the repository at this point in the history
  • Loading branch information
tarrencev committed Dec 17, 2024
1 parent b3e907a commit 9243305
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 39 deletions.
6 changes: 2 additions & 4 deletions examples/next/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -18,8 +17,7 @@ export default function Home() {
</h2>
<ColorModeToggle />
</div>
<ConnectWallet />
<Settings />
<Header />
<Profile />
<Transfer />
<ManualTransferEth />
Expand Down
135 changes: 135 additions & 0 deletions examples/next/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-full absolute top-0 left-0 p-5 flex items-center">
{showBack && (
<button
className="absolute top-5 left-5 w-6 h-6 cursor-pointer"
onClick={() => {
window.history.back();
}}
>
</button>
)}
<div className="flex-1" />
<div className="relative">
<Button
onClick={() => {
setNetworkOpen(!networkOpen);
setProfileOpen(false);
}}
disabled={lockChain}
className="flex items-center gap-2 min-w-[120px]"
>
{chainName}
<span
className={`transition-transform duration-200 ${
networkOpen ? "rotate-180" : ""
}`}
>
</span>
</Button>
{networkOpen && (
<div className="absolute right-0 top-full mt-1 bg-gray-700 shadow-lg rounded-md min-w-[160px] py-1 z-10">
{chains.map((c) => (
<button
key={c.id}
className="block w-full px-4 py-2 text-left hover:bg-gray-600 transition-colors border-b border-gray-600 last:border-0"
onClick={() => {
console.log(c.id);
setNetworkOpen(false);
}}
>
{c.name}
</button>
))}
</div>
)}
</div>
{address ? (
<div className="relative ml-2">
<Button
onClick={() => {
setProfileOpen(!profileOpen);
setNetworkOpen(false);
}}
className="flex items-center gap-2 min-w-[120px]"
>
<strong className="truncate max-w-[120px]">{address}</strong>
<span
className={`transition-transform duration-200 ${
profileOpen ? "rotate-180" : ""
}`}
>
</span>
</Button>
{profileOpen && (
<div className="absolute right-0 top-full mt-1 bg-gray-700 shadow-lg rounded-md min-w-[160px] py-1 z-10">
<button
className="block w-full px-4 py-2 text-left hover:bg-gray-600 transition-colors border-b border-gray-600"
onClick={() => controllerConnector.controller.openProfile()}
>
Profile
</button>
<button
className="block w-full px-4 py-2 text-left hover:bg-gray-600 transition-colors border-b border-gray-600"
onClick={() => controllerConnector.controller.openSettings()}
>
Settings
</button>
<button
className="block w-full px-4 py-2 text-left hover:bg-gray-600 transition-colors"
onClick={() => disconnect()}
>
Disconnect
</button>
</div>
)}
</div>
) : (
<Button
onClick={() => {
connect({ connector: connectors[0] });
}}
>
Connect
</Button>
)}
</div>
);
};

export default Header;
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,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,
Expand Down
2 changes: 1 addition & 1 deletion packages/controller/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ControllerAccount extends WalletAccount {
options: KeychainOptions,
modal: Modal,
) {
super({ nodeUrl: provider.rpc.toString() }, provider);
super(provider, provider);

this.address = address;
this.keychain = keychain;
Expand Down
21 changes: 14 additions & 7 deletions packages/controller/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import BaseProvider from "./provider";
import { WalletAccount } from "starknet";
import { Policy } from "@cartridge/presets";
import { AddStarknetChainParameters } from "@starknet-io/types-js";

export default class ControllerProvider extends BaseProvider {
private keychain?: AsyncMethodReturns<Keychain>;
Expand All @@ -25,9 +26,9 @@ export default class ControllerProvider extends BaseProvider {
private iframes: IFrames;

constructor(options: ControllerOptions) {
const { rpc } = options;
super({ rpc });
super(options);

this.rpcUrl = options.rpcUrl;
this.iframes = {
keychain: new KeychainIFrame({
...options,
Expand All @@ -54,9 +55,7 @@ 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,
Expand All @@ -83,7 +82,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,
});
Expand Down Expand Up @@ -114,7 +113,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);
Expand All @@ -137,6 +136,14 @@ export default class ControllerProvider extends BaseProvider {
}
}

switchStarknetChain(_chainId: string): Promise<boolean> {
return Promise.resolve(true);
}

addStarknetChain(_chain: AddStarknetChainParameters): Promise<boolean> {
return Promise.resolve(true);
}

async disconnect() {
if (!this.keychain) {
console.error(new NotReadyToConnect().message);
Expand Down
40 changes: 22 additions & 18 deletions packages/controller/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { WalletAccount } from "starknet";
import { Provider, WalletAccount } from "starknet";
import {
AddInvokeTransactionParameters,
AddStarknetChainParameters,
Errors,
Permission,
RequestFn,
StarknetWindowObject,
SwitchStarknetChainParameters,
TypedData,
WalletEventHandlers,
WalletEventListener,
Expand All @@ -14,20 +16,22 @@ import {
import { icon } from "./icon";
import { ProviderOptions } from "./types";

export default abstract class BaseProvider implements StarknetWindowObject {
export default abstract class BaseProvider
extends Provider
implements StarknetWindowObject
{
public id = "controller";
public name = "Controller";
public version = "0.4.0";
public icon = icon;

public rpc: URL;
public account?: WalletAccount;
public subscriptions: WalletEvents[] = [];
protected rpcUrl: string;

constructor(options: ProviderOptions) {
const { rpc } = options;

this.rpc = new URL(rpc);
super({ nodeUrl: options.rpcUrl });
this.rpcUrl = options.rpcUrl;
}

request: RequestFn = async (call) => {
Expand Down Expand Up @@ -65,19 +69,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) {
Expand Down Expand Up @@ -174,4 +174,8 @@ export default abstract class BaseProvider implements StarknetWindowObject {

abstract probe(): Promise<WalletAccount | undefined>;
abstract connect(): Promise<WalletAccount | undefined>;
abstract switchStarknetChain(chainId: string): Promise<boolean>;
abstract addStarknetChain(
chain: AddStarknetChainParameters,
): Promise<boolean>;
}
15 changes: 12 additions & 3 deletions packages/controller/src/session/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -32,7 +33,7 @@ export default class SessionProvider extends BaseProvider {
protected _policies: SessionPolicies;

constructor({ rpc, chainId, policies, redirectUrl }: SessionOptions) {
super({ rpc });
super({ rpcUrl: rpc });

this._chainId = chainId;
this._redirectUrl = redirectUrl;
Expand Down Expand Up @@ -76,14 +77,22 @@ 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");

return;
}

switchStarknetChain(_chainId: string): Promise<boolean> {
return Promise.resolve(true);
}

addStarknetChain(_chain: AddStarknetChainParameters): Promise<boolean> {
return Promise.resolve(true);
}

disconnect(): Promise<void> {
localStorage.removeItem("sessionSigner");
localStorage.removeItem("session");
Expand Down Expand Up @@ -127,7 +136,7 @@ export default class SessionProvider extends BaseProvider {

this._username = sessionRegistration.username;
this.account = new SessionAccount(this, {
rpcUrl: this.rpc.toString(),
rpcUrl: this.rpcUrl.toString(),
privateKey: signer.privKey,
address: sessionRegistration.address,
ownerGuid: sessionRegistration.ownerGuid,
Expand Down
Loading

0 comments on commit 9243305

Please sign in to comment.