add iframe support for gnosis safe provider (#8)

This commit is contained in:
Apoorv Lathey
2022-09-21 03:10:30 +05:30
committed by GitHub
parent 33b2ff2adf
commit fb3f8561c9
10 changed files with 1201 additions and 95 deletions

1
.env.sample Normal file
View File

@@ -0,0 +1 @@
REACT_APP_INFURA_KEY=

View File

@@ -0,0 +1,29 @@
import { Box } from "@chakra-ui/react";
const Tab = ({
children,
tabIndex,
selectedTabIndex,
setSelectedTabIndex,
}: {
children: React.ReactNode;
tabIndex: number;
selectedTabIndex: number;
setSelectedTabIndex: Function;
}) => {
return (
<Box
fontWeight={"semibold"}
color={tabIndex === selectedTabIndex ? "white" : "whiteAlpha.700"}
_hover={{
color: "whiteAlpha.900",
}}
cursor="pointer"
onClick={() => setSelectedTabIndex(tabIndex)}
>
{children}
</Box>
);
};
export default Tab;

View File

@@ -48,6 +48,8 @@ import WalletConnect from "@walletconnect/client";
import { IClientMeta } from "@walletconnect/types";
import { ethers } from "ethers";
import axios from "axios";
import { useSafeInject } from "../../contexts/SafeInjectContext";
import Tab from "./Tab";
import networkInfo from "./networkInfo";
const slicedText = (txt: string) => {
@@ -81,11 +83,22 @@ const TD = ({ txt }: { txt: string }) => (
function Body() {
const { colorMode } = useColorMode();
const bgColor = { light: "white", dark: "gray.700" };
const addressFromURL = new URLSearchParams(window.location.search).get("address");
const addressFromURL = new URLSearchParams(window.location.search).get(
"address"
);
const toast = useToast();
const { onOpen, onClose, isOpen } = useDisclosure();
const { isOpen: tableIsOpen, onToggle: tableOnToggle } = useDisclosure();
const {
setAddress: setIFrameAddress,
appUrl,
setAppUrl,
setRpcUrl,
iframeRef,
latestTransaction,
} = useSafeInject();
const [provider, setProvider] = useState<ethers.providers.JsonRpcProvider>();
const [showAddress, setShowAddress] = useState(addressFromURL ?? ""); // gets displayed in input. ENS name remains as it is
const [address, setAddress] = useState(addressFromURL ?? ""); // internal resolved address
@@ -97,6 +110,11 @@ function Body() {
const [isConnected, setIsConnected] = useState(false);
const [loading, setLoading] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [isIFrameLoading, setIsIFrameLoading] = useState(false);
const [inputAppUrl, setInputAppUrl] = useState<string>();
const [iframeKey, setIframeKey] = useState(0); // hacky way to reload iframe when key changes
const [tenderlyForkId, setTenderlyForkId] = useState("");
const [sendTxnData, setSendTxnData] = useState<
{
@@ -121,8 +139,9 @@ function Body() {
setUri(_connector.uri);
setPeerMeta(_connector.peerMeta);
setIsConnected(true);
const chainId = (_connector.chainId as unknown as { chainID: number })
.chainID || _connector.chainId;
const chainId =
(_connector.chainId as unknown as { chainID: number }).chainID ||
_connector.chainId;
for (let i = 0; i < networkInfo.length; i++) {
if (getChainId(i) === chainId) {
@@ -138,7 +157,9 @@ function Body() {
}
setProvider(
new ethers.providers.JsonRpcProvider(process.env.REACT_APP_PROVIDER_URL)
new ethers.providers.JsonRpcProvider(
`https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`
)
);
const storedTenderlyForkId = localStorage.getItem("tenderlyForkId");
@@ -160,6 +181,52 @@ function Body() {
localStorage.setItem("showAddress", showAddress);
}, [showAddress]);
useEffect(() => {
setIFrameAddress(address);
}, [address]);
useEffect(() => {
setRpcUrl(networkInfo[networkIndex].rpc);
}, [networkIndex]);
useEffect(() => {
if (latestTransaction) {
const newTxn = {
from: address,
...latestTransaction,
};
setSendTxnData((data) => {
if (data.some((d) => d.id === newTxn.id)) {
return data;
} else {
return [
{ ...newTxn, value: parseInt(newTxn.value, 16).toString() },
...data,
];
}
});
if (tenderlyForkId.length > 0) {
axios
.post("https://rpc.tenderly.co/fork/" + tenderlyForkId, {
jsonrpc: "2.0",
id: newTxn.id,
method: "eth_sendTransaction",
params: [
{
from: newTxn.from,
to: newTxn.to,
value: newTxn.value,
data: newTxn.data,
},
],
})
.then((res) => console.log(res.data));
}
}
}, [latestTransaction, tenderlyForkId]);
const resolveAndValidateAddress = async () => {
let isValid;
let _address = address;
@@ -244,6 +311,22 @@ function Body() {
}
};
const initIFrame = async () => {
setIsIFrameLoading(true);
if (inputAppUrl === appUrl) {
setIsIFrameLoading(false);
return;
}
const { isValid } = await resolveAndValidateAddress();
if (!isValid) {
setIsIFrameLoading(false);
return;
}
setAppUrl(inputAppUrl);
};
const subscribeToEvents = () => {
console.log("ACTION", "subscribeToEvents");
@@ -392,13 +475,35 @@ function Body() {
};
const updateAddress = async () => {
setLoading(true);
if (selectedTabIndex === 0) {
setLoading(true);
} else {
setIsIFrameLoading(true);
}
const { isValid, _address } = await resolveAndValidateAddress();
if (isValid) {
if (selectedTabIndex === 0) {
updateSession({
newAddress: _address,
});
} else {
setIFrameAddress(_address);
setIframeKey((key) => key + 1);
setIsIFrameLoading(false);
}
}
};
const updateNetwork = (_networkIndex: number) => {
setNetworkIndex(_networkIndex);
if (selectedTabIndex === 0) {
updateSession({
newAddress: _address,
newChainId: getChainId(_networkIndex),
});
} else {
setIframeKey((key) => key + 1);
}
};
@@ -482,8 +587,7 @@ function Body() {
<FormLabel>Enter Address or ENS to Impersonate</FormLabel>
<InputGroup>
<Input
placeholder="Address"
aria-label="address"
placeholder="vitalik.eth"
autoComplete="off"
value={showAddress}
onChange={(e) => {
@@ -495,7 +599,8 @@ function Body() {
bg={bgColor[colorMode]}
isInvalid={!isAddressValid}
/>
{isConnected && (
{((selectedTabIndex === 0 && isConnected) ||
(selectedTabIndex === 1 && appUrl)) && (
<InputRightElement width="4.5rem" mr="1rem">
<Button h="1.75rem" size="sm" onClick={updateAddress}>
Update
@@ -504,49 +609,15 @@ function Body() {
)}
</InputGroup>
</FormControl>
<FormControl my={4}>
<HStack>
<FormLabel>WalletConnect URI</FormLabel>
<Tooltip
label={
<>
<Text>Visit any dApp and select WalletConnect.</Text>
<Text>
Click "Copy to Clipboard" beneath the QR code, and paste it
here.
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Input
placeholder="wc:xyz123"
aria-label="uri"
autoComplete="off"
value={uri}
onChange={(e) => setUri(e.target.value)}
bg={bgColor[colorMode]}
isDisabled={isConnected}
/>
</FormControl>
<Select
mb={4}
mt={4}
placeholder="Select Network"
variant="filled"
_hover={{ cursor: "pointer" }}
value={networkIndex}
onChange={(e) => {
const _networkIndex = parseInt(e.target.value);
setNetworkIndex(_networkIndex);
updateSession({
newChainId: getChainId(_networkIndex),
});
updateNetwork(_networkIndex);
}}
>
{networkInfo.map((network, i) => (
@@ -555,56 +626,169 @@ function Body() {
</option>
))}
</Select>
<Button onClick={initWalletConnect} isDisabled={isConnected}>
Connect
</Button>
{loading && (
<Center>
<VStack>
<Box>
<CircularProgress isIndeterminate />
</Box>
{!isConnected && (
<Box pt={6}>
<Button
onClick={() => {
setLoading(false);
reset();
}}
>
Stop Loading
</Button>
</Box>
)}
</VStack>
</Center>
)}
{peerMeta && (
<Center flexDir="column">
<HStack
mt="1rem"
minH="3rem"
px="1.5rem"
spacing={"8"}
background="gray.700"
borderRadius="xl"
>
{["WalletConnect", "IFrame"].map((t, i) => (
<Tab
key={i}
tabIndex={i}
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
>
{t}
</Tab>
))}
</HStack>
</Center>
{selectedTabIndex === 0 ? (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
{isConnected ? "✅ Connected To:" : "⚠ Allow to Connect"}
</Box>
<VStack>
<Avatar src={peerMeta.icons[0]} alt={peerMeta.name} />
<Text fontWeight="bold">{peerMeta.name}</Text>
<Text fontSize="sm">{peerMeta.description}</Text>
<Link href={peerMeta.url} textDecor="underline">
{peerMeta.url}
</Link>
{!isConnected && (
<Box pt={6}>
<Button onClick={approveSession} mr={10}>
Connect
</Button>
<Button onClick={rejectSession}>Reject </Button>
<FormControl my={4}>
<HStack>
<FormLabel>WalletConnect URI</FormLabel>
<Tooltip
label={
<>
<Text>Visit any dApp and select WalletConnect.</Text>
<Text>
Click "Copy to Clipboard" beneath the QR code, and paste
it here.
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Input
placeholder="wc:xyz123"
aria-label="uri"
autoComplete="off"
value={uri}
onChange={(e) => setUri(e.target.value)}
bg={bgColor[colorMode]}
isDisabled={isConnected}
/>
</FormControl>
<Center>
<Button onClick={initWalletConnect} isDisabled={isConnected}>
Connect
</Button>
</Center>
{loading && (
<Center>
<VStack>
<Box>
<CircularProgress isIndeterminate />
</Box>
{!isConnected && (
<Box pt={6}>
<Button
onClick={() => {
setLoading(false);
reset();
}}
>
Stop Loading
</Button>
</Box>
)}
</VStack>
</Center>
)}
{peerMeta && (
<>
<Box mt={4} fontSize={24} fontWeight="semibold">
{isConnected ? "✅ Connected To:" : "⚠ Allow to Connect"}
</Box>
<VStack>
<Avatar src={peerMeta.icons[0]} alt={peerMeta.name} />
<Text fontWeight="bold">{peerMeta.name}</Text>
<Text fontSize="sm">{peerMeta.description}</Text>
<Link href={peerMeta.url} textDecor="underline">
{peerMeta.url}
</Link>
{!isConnected && (
<Box pt={6}>
<Button onClick={approveSession} mr={10}>
Connect
</Button>
<Button onClick={rejectSession}>Reject </Button>
</Box>
)}
{isConnected && (
<Box pt={6}>
<Button onClick={killSession}>Disconnect </Button>
</Box>
)}
</VStack>
</>
)}
</>
) : (
<>
<FormControl my={4}>
<HStack>
<FormLabel>dapp URL</FormLabel>
<Tooltip
label={
<>
<Text>Paste the URL of dapp you want to connect to</Text>
<Text>
Note: Some dapps might not support it, so use
WalletConnect in that case
</Text>
</>
}
hasArrow
placement="top"
>
<Box pb="0.8rem">
<InfoIcon />
</Box>
</Tooltip>
</HStack>
<Input
placeholder="https://app.uniswap.org/"
aria-label="dapp-url"
autoComplete="off"
value={inputAppUrl}
onChange={(e) => setInputAppUrl(e.target.value)}
bg={bgColor[colorMode]}
/>
</FormControl>
<Center>
<Button onClick={() => initIFrame()} isLoading={isIFrameLoading}>
Connect
</Button>
</Center>
<Center mt="1rem" ml="-60" w="70rem">
{appUrl && (
<iframe
title="app"
src={appUrl}
key={iframeKey}
width="1500rem"
height="600rem"
style={{
border: "1px solid white",
background: "white",
}}
ref={iframeRef}
onLoad={() => setIsIFrameLoading(false)}
/>
)}
{isConnected && (
<Box pt={6}>
<Button onClick={killSession}>Disconnect </Button>
</Box>
)}
</VStack>
</Center>
</>
)}
<Center>

View File

@@ -2,50 +2,62 @@ const networkInfo = [
{
chainID: 1,
name: "Ethereum Mainnet",
rpc: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
},
{
chainID: 42161,
name: "Arbitrum One",
rpc: "https://arb1.arbitrum.io/rpc",
},
{
chainID: 10,
name: "Optimistic Ethereum",
name: "Optimism",
rpc: "https://mainnet.optimism.io",
},
{
chainID: 137,
name: "Polygon",
rpc: "https://polygon-rpc.com",
},
{
chainID: 56,
name: "Binance Smart Chain",
rpc: "https://bscrpc.com",
},
{
chainID: 250,
name: "Fantom Opera",
rpc: "https://rpc.fantom.network",
},
{
chainID: 43114,
name: "Avalanche",
rpc: "https://rpc.ankr.com/avalanche",
},
{
chainID: 100,
name: "xDAI",
name: "Gnosis",
rpc: "https://rpc.ankr.com/gnosis",
},
{
chainID: 42,
name: "Kovan Testnet",
rpc: `https://kovan.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
},
{
chainID: 3,
name: "Ropsten Testnet",
rpc: `https://ropsten.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
},
{
chainID: 4,
name: "Rinkeby Testnet",
rpc: `https://rinkeby.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
},
{
chainID: 5,
name: "Goerli Testnet",
rpc: `https://goerli.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
},
];

View File

@@ -0,0 +1,171 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
useCallback,
} from "react";
import { providers, utils } from "ethers";
import { useAppCommunicator } from "../helpers/communicator";
import {
InterfaceMessageIds,
InterfaceMessageProps,
Methods,
MethodToResponse,
RequestId,
RPCPayload,
SignMessageParams,
SignTypedMessageParams,
Transaction,
} from "../types";
interface TransactionWithId extends Transaction {
id: number;
}
type SafeInjectContextType = {
address: string | undefined;
appUrl: string | undefined;
rpcUrl: string | undefined;
iframeRef: React.RefObject<HTMLIFrameElement> | null;
latestTransaction: TransactionWithId | undefined;
setAddress: React.Dispatch<React.SetStateAction<string | undefined>>;
setAppUrl: React.Dispatch<React.SetStateAction<string | undefined>>;
setRpcUrl: React.Dispatch<React.SetStateAction<string | undefined>>;
sendMessageToIFrame: Function;
};
export const SafeInjectContext = createContext<SafeInjectContextType>({
address: undefined,
appUrl: undefined,
rpcUrl: undefined,
iframeRef: null,
latestTransaction: undefined,
setAddress: () => {},
setAppUrl: () => {},
setRpcUrl: () => {},
sendMessageToIFrame: () => {},
});
export interface FCProps {
children: React.ReactNode;
}
export const SafeInjectProvider: React.FunctionComponent<FCProps> = ({
children,
}) => {
const [address, setAddress] = useState<string>();
const [appUrl, setAppUrl] = useState<string>();
const [rpcUrl, setRpcUrl] = useState<string>();
const [provider, setProvider] = useState<providers.StaticJsonRpcProvider>();
const [latestTransaction, setLatestTransaction] =
useState<TransactionWithId>();
const iframeRef = useRef<HTMLIFrameElement>(null);
const communicator = useAppCommunicator(iframeRef);
const sendMessageToIFrame = useCallback(
function <T extends InterfaceMessageIds>(
message: InterfaceMessageProps<T>,
requestId?: RequestId
) {
const requestWithMessage = {
...message,
requestId: requestId || Math.trunc(window.performance.now()),
version: "0.4.2",
};
if (iframeRef) {
iframeRef.current?.contentWindow?.postMessage(
requestWithMessage,
appUrl!
);
}
},
[iframeRef, appUrl]
);
useEffect(() => {
if (!rpcUrl) return;
setProvider(new providers.StaticJsonRpcProvider(rpcUrl));
}, [rpcUrl]);
useEffect(() => {
if (!provider) return;
communicator?.on(Methods.getSafeInfo, async () => ({
safeAddress: address,
chainId: (await provider.getNetwork()).chainId,
owners: [],
threshold: 1,
isReadOnly: false,
}));
communicator?.on(Methods.getEnvironmentInfo, async () => ({
origin: document.location.origin,
}));
communicator?.on(Methods.rpcCall, async (msg) => {
const params = msg.data.params as RPCPayload;
try {
const response = (await provider.send(
params.call,
params.params
)) as MethodToResponse["rpcCall"];
return response;
} catch (err) {
return err;
}
});
communicator?.on(Methods.sendTransactions, (msg) => {
// @ts-expect-error explore ways to fix this
const transactions = (msg.data.params.txs as Transaction[]).map(
({ to, ...rest }) => ({
to: utils.getAddress(to), // checksummed
...rest,
})
);
setLatestTransaction({
id: parseInt(msg.data.id.toString()),
...transactions[0],
});
// openConfirmationModal(transactions, msg.data.params.params, msg.data.id)
});
communicator?.on(Methods.signMessage, async (msg) => {
const { message } = msg.data.params as SignMessageParams;
// openSignMessageModal(message, msg.data.id, Methods.signMessage)
});
communicator?.on(Methods.signTypedMessage, async (msg) => {
const { typedData } = msg.data.params as SignTypedMessageParams;
// openSignMessageModal(typedData, msg.data.id, Methods.signTypedMessage)
});
}, [communicator, address, provider]);
return (
<SafeInjectContext.Provider
value={{
address,
appUrl,
rpcUrl,
iframeRef,
latestTransaction,
setAddress,
setAppUrl,
setRpcUrl,
sendMessageToIFrame,
}}
>
{children}
</SafeInjectContext.Provider>
);
};
export const useSafeInject = () => useContext(SafeInjectContext);

120
src/helpers/communicator.ts Normal file
View File

@@ -0,0 +1,120 @@
import { MutableRefObject, useEffect, useState } from "react";
import { MessageFormatter } from "./messageFormatter";
import {
SDKMessageEvent,
MethodToResponse,
Methods,
ErrorResponse,
RequestId,
} from "../types";
import { getSDKVersion } from "./utils";
type MessageHandler = (
msg: SDKMessageEvent
) =>
| void
| MethodToResponse[Methods]
| ErrorResponse
| Promise<MethodToResponse[Methods] | ErrorResponse | void>;
export enum LegacyMethods {
getEnvInfo = "getEnvInfo",
}
type SDKMethods = Methods | LegacyMethods;
class AppCommunicator {
private iframeRef: MutableRefObject<HTMLIFrameElement | null>;
private handlers = new Map<SDKMethods, MessageHandler>();
constructor(iframeRef: MutableRefObject<HTMLIFrameElement | null>) {
this.iframeRef = iframeRef;
window.addEventListener("message", this.handleIncomingMessage);
}
on = (method: SDKMethods, handler: MessageHandler): void => {
this.handlers.set(method, handler);
};
private isValidMessage = (msg: SDKMessageEvent): boolean => {
if (msg.data.hasOwnProperty("isCookieEnabled")) {
return true;
}
const sentFromIframe = this.iframeRef.current?.contentWindow === msg.source;
const knownMethod = Object.values(Methods).includes(msg.data.method);
return sentFromIframe && knownMethod;
};
private canHandleMessage = (msg: SDKMessageEvent): boolean => {
return Boolean(this.handlers.get(msg.data.method));
};
send = (data: unknown, requestId: RequestId, error = false): void => {
const sdkVersion = getSDKVersion();
const msg = error
? MessageFormatter.makeErrorResponse(
requestId,
data as string,
sdkVersion
)
: MessageFormatter.makeResponse(requestId, data, sdkVersion);
// console.log("send", { msg });
this.iframeRef.current?.contentWindow?.postMessage(msg, "*");
};
handleIncomingMessage = async (msg: SDKMessageEvent): Promise<void> => {
const validMessage = this.isValidMessage(msg);
const hasHandler = this.canHandleMessage(msg);
if (validMessage && hasHandler) {
// console.log("incoming", { msg: msg.data });
const handler = this.handlers.get(msg.data.method);
try {
// @ts-expect-error Handler existence is checked in this.canHandleMessage
const response = await handler(msg);
// If response is not returned, it means the response will be send somewhere else
if (typeof response !== "undefined") {
this.send(response, msg.data.id);
}
} catch (err: any) {
this.send(err.message, msg.data.id, true);
}
}
};
clear = (): void => {
window.removeEventListener("message", this.handleIncomingMessage);
};
}
const useAppCommunicator = (
iframeRef: MutableRefObject<HTMLIFrameElement | null>
): AppCommunicator | undefined => {
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(
undefined
);
useEffect(() => {
let communicatorInstance: AppCommunicator;
const initCommunicator = (
iframeRef: MutableRefObject<HTMLIFrameElement>
) => {
communicatorInstance = new AppCommunicator(iframeRef);
setCommunicator(communicatorInstance);
};
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>);
return () => {
communicatorInstance?.clear();
};
}, [iframeRef]);
return communicator;
};
export { useAppCommunicator };

View File

@@ -0,0 +1,51 @@
import {
ErrorResponse,
SDKRequestData,
RequestId,
SuccessResponse,
MethodToResponse,
Methods,
} from "../types";
import { getSDKVersion, generateRequestId } from "./utils";
class MessageFormatter {
static makeRequest = <M extends Methods = Methods, P = unknown>(
method: M,
params: P
): SDKRequestData<M, P> => {
const id = generateRequestId();
return {
id,
method,
params,
env: {
sdkVersion: getSDKVersion(),
},
};
};
static makeResponse = (
id: RequestId,
data: MethodToResponse[Methods],
version: string
): SuccessResponse => ({
id,
success: true,
version,
data,
});
static makeErrorResponse = (
id: RequestId,
error: string,
version: string
): ErrorResponse => ({
id,
success: false,
error,
version,
});
}
export { MessageFormatter };

20
src/helpers/utils.ts Normal file
View File

@@ -0,0 +1,20 @@
export const getSDKVersion = () => {
return "7.6.0"; // IMPORTANT: needs to be >= 1.0.0
};
// i.e. 0-255 -> '00'-'ff'
const dec2hex = (dec: number): string => dec.toString(16).padStart(2, "0");
const generateId = (len: number): string => {
const arr = new Uint8Array((len || 40) / 2);
window.crypto.getRandomValues(arr);
return Array.from(arr, dec2hex).join("");
};
export const generateRequestId = (): string => {
if (typeof window !== "undefined") {
return generateId(10);
}
return new Date().getTime().toString(36);
};

View File

@@ -2,10 +2,13 @@ import ReactDOM from "react-dom";
import App from "./App";
import { ChakraProvider } from "@chakra-ui/react";
import theme from "./theme";
import { SafeInjectProvider } from "./contexts/SafeInjectContext";
ReactDOM.render(
<ChakraProvider theme={theme}>
<App />
<SafeInjectProvider>
<App />
</SafeInjectProvider>
</ChakraProvider>,
document.getElementById("root")
);

515
src/types.ts Normal file
View File

@@ -0,0 +1,515 @@
import { BigNumberish, BytesLike } from "ethers";
export declare const INTERFACE_MESSAGES: {
readonly ENV_INFO: "ENV_INFO";
readonly ON_SAFE_INFO: "ON_SAFE_INFO";
readonly TRANSACTION_CONFIRMED: "TRANSACTION_CONFIRMED";
readonly TRANSACTION_REJECTED: "TRANSACTION_REJECTED";
};
export type InterfaceMessageIds = keyof typeof INTERFACE_MESSAGES;
export declare type LowercaseNetworks =
| "mainnet"
| "morden"
| "ropsten"
| "rinkeby"
| "goerli"
| "kovan"
| "xdai"
| "energy_web_chain"
| "volta"
| "unknown";
export interface SafeInfo {
safeAddress: string;
network: LowercaseNetworks;
ethBalance: string;
}
export interface InterfaceMessageToPayload {
[INTERFACE_MESSAGES.ON_SAFE_INFO]: SafeInfo;
[INTERFACE_MESSAGES.TRANSACTION_CONFIRMED]: {
safeTxHash: string;
};
[INTERFACE_MESSAGES.ENV_INFO]: {
txServiceUrl: string;
};
[INTERFACE_MESSAGES.TRANSACTION_REJECTED]: Record<string, unknown>;
}
export type InterfaceMessageProps<T extends InterfaceMessageIds> = {
messageId: T;
data: InterfaceMessageToPayload[T];
};
export declare type RequestId = number | string;
// Messaging
export enum Methods {
sendTransactions = "sendTransactions",
rpcCall = "rpcCall",
getChainInfo = "getChainInfo",
getSafeInfo = "getSafeInfo",
getTxBySafeTxHash = "getTxBySafeTxHash",
getSafeBalances = "getSafeBalances",
signMessage = "signMessage",
signTypedMessage = "signTypedMessage",
getEnvironmentInfo = "getEnvironmentInfo",
requestAddressBook = "requestAddressBook",
wallet_getPermissions = "wallet_getPermissions",
wallet_requestPermissions = "wallet_requestPermissions",
}
export declare type SDKRequestData<M extends Methods = Methods, P = unknown> = {
id: RequestId;
params: P;
env: {
sdkVersion: string;
};
method: M;
};
export declare type SDKMessageEvent = MessageEvent<SDKRequestData>;
export declare type SendTransactionsResponse = {
safeTxHash: string;
};
export enum RPC_AUTHENTICATION {
API_KEY_PATH = "API_KEY_PATH",
NO_AUTHENTICATION = "NO_AUTHENTICATION",
UNKNOWN = "UNKNOWN",
}
export type RpcUri = {
authentication: RPC_AUTHENTICATION;
value: string;
};
export type BlockExplorerUriTemplate = {
address: string;
txHash: string;
api: string;
};
export type NativeCurrency = {
name: string;
symbol: string;
decimals: number;
logoUri: string;
};
export type Theme = {
textColor: string;
backgroundColor: string;
};
export enum GAS_PRICE_TYPE {
ORACLE = "ORACLE",
FIXED = "FIXED",
UNKNOWN = "UNKNOWN",
}
export type GasPriceOracle = {
type: GAS_PRICE_TYPE.ORACLE;
uri: string;
gasParameter: string;
gweiFactor: string;
};
export type GasPriceFixed = {
type: GAS_PRICE_TYPE.FIXED;
weiValue: string;
};
export type GasPriceUnknown = {
type: GAS_PRICE_TYPE.UNKNOWN;
};
export type GasPrice = (GasPriceOracle | GasPriceFixed | GasPriceUnknown)[];
export enum FEATURES {
ERC721 = "ERC721",
SAFE_APPS = "SAFE_APPS",
CONTRACT_INTERACTION = "CONTRACT_INTERACTION",
DOMAIN_LOOKUP = "DOMAIN_LOOKUP",
SPENDING_LIMIT = "SPENDING_LIMIT",
EIP1559 = "EIP1559",
SAFE_TX_GAS_OPTIONAL = "SAFE_TX_GAS_OPTIONAL",
TX_SIMULATION = "TX_SIMULATION",
}
export type _ChainInfo = {
transactionService: string;
chainId: string; // Restricted by what is returned by the CGW
chainName: string;
shortName: string;
l2: boolean;
description: string;
rpcUri: RpcUri;
safeAppsRpcUri: RpcUri;
publicRpcUri: RpcUri;
blockExplorerUriTemplate: BlockExplorerUriTemplate;
nativeCurrency: NativeCurrency;
theme: Theme;
ensRegistryAddress?: string;
gasPrice: GasPrice;
disabledWallets: string[];
features: FEATURES[];
};
export declare type ChainInfo = Pick<
_ChainInfo,
| "chainName"
| "chainId"
| "shortName"
| "nativeCurrency"
| "blockExplorerUriTemplate"
>;
export enum TransactionStatus {
AWAITING_CONFIRMATIONS = "AWAITING_CONFIRMATIONS",
AWAITING_EXECUTION = "AWAITING_EXECUTION",
CANCELLED = "CANCELLED",
FAILED = "FAILED",
SUCCESS = "SUCCESS",
PENDING = "PENDING",
WILL_BE_REPLACED = "WILL_BE_REPLACED",
}
export type AddressEx = {
value: string;
name?: string;
logoUri?: string;
};
export enum TransferDirection {
INCOMING = "INCOMING",
OUTGOING = "OUTGOING",
UNKNOWN = "UNKNOWN",
}
export enum TransactionTokenType {
ERC20 = "ERC20",
ERC721 = "ERC721",
NATIVE_COIN = "NATIVE_COIN",
}
export type Erc20Transfer = {
type: TransactionTokenType.ERC20;
tokenAddress: string;
tokenName?: string;
tokenSymbol?: string;
logoUri?: string;
decimals?: number;
value: string;
};
export type Erc721Transfer = {
type: TransactionTokenType.ERC721;
tokenAddress: string;
tokenId: string;
tokenName?: string;
tokenSymbol?: string;
logoUri?: string;
};
export type NativeCoinTransfer = {
type: TransactionTokenType.NATIVE_COIN;
value: string;
};
export type TransferInfo = Erc20Transfer | Erc721Transfer | NativeCoinTransfer;
export interface Transfer {
type: "Transfer";
sender: AddressEx;
recipient: AddressEx;
direction: TransferDirection;
transferInfo: TransferInfo;
}
export type ParamValue = string | ParamValue[];
export enum Operation {
CALL = 0,
DELEGATE = 1,
}
export type InternalTransaction = {
operation: Operation;
to: string;
value?: string;
data?: string;
dataDecoded?: DataDecoded;
};
export type ValueDecodedType = InternalTransaction[];
export type Parameter = {
name: string;
type: string;
value: ParamValue;
valueDecoded?: ValueDecodedType;
};
export type DataDecoded = {
method: string;
parameters?: Parameter[];
};
export enum SettingsInfoType {
SET_FALLBACK_HANDLER = "SET_FALLBACK_HANDLER",
ADD_OWNER = "ADD_OWNER",
REMOVE_OWNER = "REMOVE_OWNER",
SWAP_OWNER = "SWAP_OWNER",
CHANGE_THRESHOLD = "CHANGE_THRESHOLD",
CHANGE_IMPLEMENTATION = "CHANGE_IMPLEMENTATION",
ENABLE_MODULE = "ENABLE_MODULE",
DISABLE_MODULE = "DISABLE_MODULE",
SET_GUARD = "SET_GUARD",
DELETE_GUARD = "DELETE_GUARD",
}
export type SetFallbackHandler = {
type: SettingsInfoType.SET_FALLBACK_HANDLER;
handler: AddressEx;
};
export type AddOwner = {
type: SettingsInfoType.ADD_OWNER;
owner: AddressEx;
threshold: number;
};
export type SettingsInfo =
| SetFallbackHandler
| AddOwner
| RemoveOwner
| SwapOwner
| ChangeThreshold
| ChangeImplementation
| EnableModule
| DisableModule
| SetGuard
| DeleteGuard;
export type RemoveOwner = {
type: SettingsInfoType.REMOVE_OWNER;
owner: AddressEx;
threshold: number;
};
export type SwapOwner = {
type: SettingsInfoType.SWAP_OWNER;
oldOwner: AddressEx;
newOwner: AddressEx;
};
export type ChangeThreshold = {
type: SettingsInfoType.CHANGE_THRESHOLD;
threshold: number;
};
export type ChangeImplementation = {
type: SettingsInfoType.CHANGE_IMPLEMENTATION;
implementation: AddressEx;
};
export type EnableModule = {
type: SettingsInfoType.ENABLE_MODULE;
module: AddressEx;
};
export type DisableModule = {
type: SettingsInfoType.DISABLE_MODULE;
module: AddressEx;
};
export type SetGuard = {
type: SettingsInfoType.SET_GUARD;
guard: AddressEx;
};
export type DeleteGuard = {
type: SettingsInfoType.DELETE_GUARD;
};
export type SettingsChange = {
type: "SettingsChange";
dataDecoded: DataDecoded;
settingsInfo?: SettingsInfo;
};
export interface Custom {
type: "Custom";
to: AddressEx;
dataSize: string;
value: string;
methodName?: string;
actionCount?: number;
isCancellation: boolean;
}
export type MultiSend = {
type: "Custom";
to: AddressEx;
dataSize: string;
value: string;
methodName: "multiSend";
actionCount: number;
isCancellation: boolean;
};
export type Cancellation = Custom & {
isCancellation: true;
};
export type Creation = {
type: "Creation";
creator: AddressEx;
transactionHash: string;
implementation?: AddressEx;
factory?: AddressEx;
};
export type TransactionInfo =
| Transfer
| SettingsChange
| Custom
| MultiSend
| Cancellation
| Creation;
export type TransactionData = {
hexData?: string;
dataDecoded?: DataDecoded;
to: AddressEx;
value?: string;
operation: Operation;
addressInfoIndex?: { [key: string]: AddressEx };
trustedDelegateCallTarget: boolean;
};
export type ModuleExecutionDetails = {
type: "MODULE";
address: AddressEx;
};
export type MultisigConfirmation = {
signer: AddressEx;
signature?: string;
submittedAt: number;
};
export enum TokenType {
ERC20 = "ERC20",
ERC721 = "ERC721",
NATIVE_TOKEN = "NATIVE_TOKEN",
}
export type TokenInfo = {
type: TokenType;
address: string;
decimals: number;
symbol: string;
name: string;
logoUri: string;
};
export type MultisigExecutionDetails = {
type: "MULTISIG";
submittedAt: number;
nonce: number;
safeTxGas: string;
baseGas: string;
gasPrice: string;
gasToken: string;
refundReceiver: AddressEx;
safeTxHash: string;
executor?: AddressEx;
signers: AddressEx[];
confirmationsRequired: number;
confirmations: MultisigConfirmation[];
rejectors?: AddressEx[];
gasTokenInfo?: TokenInfo;
};
export type DetailedExecutionInfo =
| ModuleExecutionDetails
| MultisigExecutionDetails;
export type SafeAppInfo = {
name: string;
url: string;
logoUri: string;
};
export type TransactionDetails = {
txId: string;
executedAt?: number;
txStatus: TransactionStatus;
txInfo: TransactionInfo;
txData?: TransactionData;
detailedExecutionInfo?: DetailedExecutionInfo;
txHash?: string;
safeAppInfo?: SafeAppInfo;
};
export declare type GatewayTransactionDetails = TransactionDetails;
export type SafeBalanceResponse = {
fiatTotal: string;
items: Array<{
tokenInfo: TokenInfo;
balance: string;
fiatBalance: string;
fiatConversion: string;
}>;
};
export declare type SafeBalances = SafeBalanceResponse;
export declare type EnvironmentInfo = {
origin: string;
};
export declare type AddressBookItem = {
address: string;
chainId: string;
name: string;
};
export declare type PermissionCaveat = {
type: string;
value?: unknown;
name?: string;
};
export declare type Permission = {
parentCapability: string;
invoker: string;
date?: number;
caveats?: PermissionCaveat[];
};
export interface MethodToResponse {
[Methods.sendTransactions]: SendTransactionsResponse;
[Methods.rpcCall]: unknown;
[Methods.getSafeInfo]: SafeInfo;
[Methods.getChainInfo]: ChainInfo;
[Methods.getTxBySafeTxHash]: GatewayTransactionDetails;
[Methods.getSafeBalances]: SafeBalances[];
[Methods.signMessage]: SendTransactionsResponse;
[Methods.signTypedMessage]: SendTransactionsResponse;
[Methods.getEnvironmentInfo]: EnvironmentInfo;
[Methods.requestAddressBook]: AddressBookItem[];
[Methods.wallet_getPermissions]: Permission[];
[Methods.wallet_requestPermissions]: Permission[];
}
export declare type ErrorResponse = {
id: RequestId;
success: false;
error: string;
version?: string;
};
export type SuccessResponse<T = MethodToResponse[Methods]> = {
id: RequestId;
data: T;
version?: string;
success: true;
};
export declare const RPC_CALLS: {
readonly eth_call: "eth_call";
readonly eth_gasPrice: "eth_gasPrice";
readonly eth_getLogs: "eth_getLogs";
readonly eth_getBalance: "eth_getBalance";
readonly eth_getCode: "eth_getCode";
readonly eth_getBlockByHash: "eth_getBlockByHash";
readonly eth_getBlockByNumber: "eth_getBlockByNumber";
readonly eth_getStorageAt: "eth_getStorageAt";
readonly eth_getTransactionByHash: "eth_getTransactionByHash";
readonly eth_getTransactionReceipt: "eth_getTransactionReceipt";
readonly eth_getTransactionCount: "eth_getTransactionCount";
readonly eth_estimateGas: "eth_estimateGas";
};
export declare type RpcCallNames = keyof typeof RPC_CALLS;
export declare type RPCPayload<P = unknown[]> = {
call: RpcCallNames;
params: P | unknown[];
};
export interface MethodToResponse {
[Methods.sendTransactions]: SendTransactionsResponse;
[Methods.rpcCall]: unknown;
[Methods.getSafeInfo]: SafeInfo;
[Methods.getChainInfo]: ChainInfo;
[Methods.getTxBySafeTxHash]: GatewayTransactionDetails;
[Methods.getSafeBalances]: SafeBalances[];
[Methods.signMessage]: SendTransactionsResponse;
[Methods.signTypedMessage]: SendTransactionsResponse;
[Methods.getEnvironmentInfo]: EnvironmentInfo;
[Methods.requestAddressBook]: AddressBookItem[];
[Methods.wallet_getPermissions]: Permission[];
[Methods.wallet_requestPermissions]: Permission[];
}
export declare type SignMessageParams = {
message: string;
};
export interface TypedDataDomain {
name?: string;
version?: string;
chainId?: BigNumberish;
verifyingContract?: string;
salt?: BytesLike;
}
export interface TypedDataTypes {
name: string;
type: string;
}
export declare type TypedMessageTypes = {
[key: string]: TypedDataTypes[];
};
export declare type EIP712TypedData = {
domain: TypedDataDomain;
types: TypedMessageTypes;
message: Record<string, any>;
};
export declare type SignTypedMessageParams = {
typedData: EIP712TypedData;
};
export interface Transaction {
to: string;
value: string;
data: string;
}