Add component to support signing cosmos tx with custom messages (#27)
Part of https://www.notion.so/Stage0-onboarding-flow-1e4a6b22d47280aba3b5da3ed1154ff5 Co-authored-by: Shreerang Kale <shreerangkale@gmail.com> Reviewed-on: LaconicNetwork/laconic-wallet-web#27 Co-authored-by: shreerang <shreerang@noreply.git.vdb.to> Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
This commit is contained in:
parent
e24d697c6d
commit
36208870ab
@ -6,6 +6,7 @@
|
||||
"@cerc-io/registry-sdk": "^0.2.5",
|
||||
"@cosmjs/amino": "^0.32.3",
|
||||
"@cosmjs/crypto": "^0.32.3",
|
||||
"@cosmjs/encoding": "^0.33.1",
|
||||
"@cosmjs/proto-signing": "^0.32.3",
|
||||
"@cosmjs/stargate": "^0.32.3",
|
||||
"@emotion/react": "^11.13.0",
|
||||
@ -28,6 +29,7 @@
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "5.7.2",
|
||||
"https-browserify": "^1.0.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -75,6 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^18.0.0",
|
||||
|
16
src/App.tsx
16
src/App.tsx
@ -30,7 +30,7 @@ import { getSignParamsMessage } from "./utils/wallet-connect/helpers";
|
||||
import ApproveTransfer from "./screens/ApproveTransfer";
|
||||
import AddNetwork from "./screens/AddNetwork";
|
||||
import EditNetwork from "./screens/EditNetwork";
|
||||
import { COSMOS, EIP155 } from "./utils/constants";
|
||||
import { CHECK_BALANCE, COSMOS, EIP155, IS_SUFFICIENT } from "./utils/constants";
|
||||
import { useNetworks } from "./context/NetworksContext";
|
||||
import { NETWORK_METHODS } from "./utils/wallet-connect/common-data";
|
||||
import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData";
|
||||
@ -44,6 +44,7 @@ import { useWebViewHandler } from "./hooks/useWebViewHandler";
|
||||
import SignRequestEmbed from "./screens/SignRequestEmbed";
|
||||
import useAddAccountEmbed from "./hooks/useAddAccountEmbed";
|
||||
import useExportPKEmbed from "./hooks/useExportPrivateKeyEmbed";
|
||||
import { SignTxEmbed } from "./screens/SignTxEmbed";
|
||||
|
||||
const Stack = createStackNavigator<StackParamsList>();
|
||||
|
||||
@ -231,7 +232,7 @@ const App = (): React.JSX.Element => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleCheckBalance = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'CHECK_BALANCE') return;
|
||||
if (event.data.type !== CHECK_BALANCE) return;
|
||||
|
||||
const { chainId, amount } = event.data;
|
||||
const network = networksData.find(net => net.chainId === chainId);
|
||||
@ -271,7 +272,7 @@ const App = (): React.JSX.Element => {
|
||||
|
||||
const areFundsSufficient = checkSufficientFunds(amount, balance.amount);
|
||||
|
||||
sendMessage(event.source as Window, 'IS_SUFFICIENT', areFundsSufficient, event.origin);
|
||||
sendMessage(event.source as Window, IS_SUFFICIENT, areFundsSufficient, event.origin);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleCheckBalance);
|
||||
@ -388,6 +389,13 @@ const App = (): React.JSX.Element => {
|
||||
header: () => <></>,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="sign-tx-request-embed"
|
||||
component={SignTxEmbed}
|
||||
options={{
|
||||
header: () => <></>,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="auto-sign-in"
|
||||
component={AutoSignIn}
|
||||
@ -396,7 +404,7 @@ const App = (): React.JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="sign-request-embed"
|
||||
name="sign-message-request-embed"
|
||||
component={SignRequestEmbed}
|
||||
options={{
|
||||
header: () => <Header title="Wallet" />,
|
||||
|
@ -6,6 +6,7 @@ import useAccountsData from '../hooks/useAccountsData';
|
||||
import { addAccount } from '../utils/accounts';
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { Account, NetworksDataState } from '../types';
|
||||
import { ADD_ACCOUNT_RESPONSE, REQUEST_ADD_ACCOUNT } from '../utils/constants';
|
||||
|
||||
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
|
||||
|
||||
@ -24,7 +25,7 @@ const useAddAccountEmbed = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleAddAccount = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'ADD_ACCOUNT') return;
|
||||
if (event.data.type !== REQUEST_ADD_ACCOUNT) return;
|
||||
|
||||
if (!REACT_APP_ALLOWED_URLS) {
|
||||
console.log('Unauthorized app origin:', event.origin);
|
||||
@ -44,7 +45,7 @@ const useAddAccountEmbed = () => {
|
||||
const updatedAccounts = await getAccountsData(event.data.chainId);
|
||||
const addresses = updatedAccounts.map((account: Account) => account.address);
|
||||
|
||||
sendMessage(event.source as Window, 'ADD_ACCOUNT_RESPONSE', addresses, event.origin);
|
||||
sendMessage(event.source as Window, ADD_ACCOUNT_RESPONSE, addresses, event.origin);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleAddAccount);
|
||||
|
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import { useAccounts } from '../context/AccountsContext';
|
||||
import { getPathKey, sendMessage } from '../utils/misc';
|
||||
import { ACCOUNT_PK_RESPONSE, REQUEST_ACCOUNT_PK } from '../utils/constants';
|
||||
|
||||
const useExportPKEmbed = () => {
|
||||
const { accounts } = useAccounts();
|
||||
@ -10,7 +11,7 @@ const useExportPKEmbed = () => {
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
const { type, chainId, address } = event.data;
|
||||
|
||||
if (type !== 'REQUEST_ACCOUNT_PK') return;
|
||||
if (type !== REQUEST_ACCOUNT_PK) return;
|
||||
|
||||
try {
|
||||
const selectedAccount = accounts.find(account => account.address === address);
|
||||
@ -23,7 +24,7 @@ const useExportPKEmbed = () => {
|
||||
|
||||
sendMessage(
|
||||
event.source as Window,
|
||||
'ACCOUNT_PK_DATA',
|
||||
ACCOUNT_PK_RESPONSE,
|
||||
{ privateKey },
|
||||
event.origin,
|
||||
);
|
||||
|
@ -5,6 +5,7 @@ import { sendMessage } from "../utils/misc";
|
||||
import useAccountsData from "./useAccountsData";
|
||||
import { useNetworks } from "../context/NetworksContext";
|
||||
import { useAccounts } from "../context/AccountsContext";
|
||||
import { REQUEST_CREATE_OR_GET_ACCOUNTS, WALLET_ACCOUNTS_DATA } from "../utils/constants";
|
||||
|
||||
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
|
||||
|
||||
@ -31,7 +32,7 @@ const useGetOrCreateAccounts = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleCreateAccounts = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return;
|
||||
if (event.data.type !== REQUEST_CREATE_OR_GET_ACCOUNTS) return;
|
||||
|
||||
if (!REACT_APP_ALLOWED_URLS) {
|
||||
console.log('Allowed URLs are not set');
|
||||
@ -50,7 +51,7 @@ const useGetOrCreateAccounts = () => {
|
||||
console.log('Sending WALLET_ACCOUNTS_DATA accounts:', accountsAddressList);
|
||||
|
||||
sendMessage(
|
||||
event.source as Window, 'WALLET_ACCOUNTS_DATA',
|
||||
event.source as Window, WALLET_ACCOUNTS_DATA,
|
||||
accountsAddressList,
|
||||
event.origin
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
INVALID_URL_ERROR,
|
||||
IS_NUMBER_REGEX,
|
||||
} from "../utils/constants";
|
||||
import { getCosmosAccounts } from "../utils/accounts";
|
||||
import { getCosmosAccountByHDPath } from "../utils/accounts";
|
||||
import ETH_CHAINS from "../assets/ethereum-chains.json";
|
||||
import {
|
||||
getInternetCredentials,
|
||||
@ -163,7 +163,7 @@ const AddNetwork = () => {
|
||||
|
||||
case COSMOS:
|
||||
address = (
|
||||
await getCosmosAccounts(
|
||||
await getCosmosAccountByHDPath(
|
||||
mnemonic,
|
||||
hdPath,
|
||||
(newNetworkData as z.infer<typeof cosmosNetworkDataSchema>)
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { signMessage } from '../utils/sign-message';
|
||||
import { EIP155 } from '../utils/constants';
|
||||
import { AUTO_SIGN_IN, EIP155, SIGN_IN_RESPONSE } from '../utils/constants';
|
||||
import { sendMessage } from '../utils/misc';
|
||||
import useAccountsData from '../hooks/useAccountsData';
|
||||
import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts';
|
||||
@ -16,7 +16,7 @@ export const AutoSignIn = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleSignIn = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'AUTO_SIGN_IN') return;
|
||||
if (event.data.type !== AUTO_SIGN_IN) return;
|
||||
|
||||
if (!REACT_APP_ALLOWED_URLS) {
|
||||
console.log('Allowed URLs are not set');
|
||||
@ -38,7 +38,7 @@ export const AutoSignIn = () => {
|
||||
|
||||
const signature = await signMessage({ message: event.data.message, accountId: accountsData[0].index, chainId: event.data.chainId, namespace: EIP155 })
|
||||
|
||||
sendMessage(event.source as Window, 'SIGN_IN_RESPONSE', { message: event.data.message, signature }, event.origin);
|
||||
sendMessage(event.source as Window, SIGN_IN_RESPONSE, { message: event.data.message, signature }, event.origin);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleSignIn);
|
||||
|
@ -12,18 +12,20 @@ import { getHeaderTitle } from '@react-navigation/elements';
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { getCosmosAccounts, retrieveSingleAccount } from '../utils/accounts';
|
||||
import { getCosmosAccountByHDPath, retrieveSingleAccount } from '../utils/accounts';
|
||||
import { getMnemonic, getPathKey, sendMessage } from '../utils/misc';
|
||||
import { COSMOS } from '../utils/constants';
|
||||
import { COSMOS, REQUEST_SIGN_MESSAGE, SIGN_MESSAGE_RESPONSE } from '../utils/constants';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
|
||||
|
||||
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'sign-request-embed'>;
|
||||
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'sign-message-request-embed'>;
|
||||
|
||||
const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
const [displayAccount, setDisplayAccount] = useState<Account>();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [chainId, setChainId] = useState<string>('');
|
||||
const [namespace, setNamespace] = useState<string>('');
|
||||
const [signDoc, setSignDoc] = useState<any>(null);
|
||||
const [signerAddress, setSignerAddress] = useState<string>('');
|
||||
const [origin, setOrigin] = useState<string>('');
|
||||
@ -31,6 +33,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isApproving, setIsApproving] = useState(false);
|
||||
|
||||
const { networksData } = useNetworks();
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
@ -39,10 +42,21 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
|
||||
setIsApproving(true);
|
||||
try {
|
||||
const requestAccount = await retrieveSingleAccount(COSMOS, chainId, signerAddress);
|
||||
const path = (await getPathKey(`${COSMOS}:${chainId}`, requestAccount!.index)).path;
|
||||
if (namespace !== COSMOS) {
|
||||
// TODO: Support ETH namespace
|
||||
throw new Error(`namespace ${namespace} is not supported`)
|
||||
}
|
||||
|
||||
const requestAccount = await retrieveSingleAccount(namespace, chainId, signerAddress);
|
||||
const path = (await getPathKey(`${namespace}:${chainId}`, requestAccount!.index)).path;
|
||||
const mnemonic = await getMnemonic();
|
||||
const cosmosAccount = await getCosmosAccounts(mnemonic, path, 'zenith');
|
||||
|
||||
const requestedNetworkData = networksData.find(networkData => networkData.chainId === chainId)
|
||||
if (!requestedNetworkData) {
|
||||
throw new Error("Requested network not found")
|
||||
}
|
||||
|
||||
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, requestedNetworkData?.addressPrefix);
|
||||
|
||||
const cosmosAminoSignature = await cosmosAccount.cosmosWallet.signAmino(
|
||||
signerAddress,
|
||||
@ -53,7 +67,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
|
||||
sendMessage(
|
||||
sourceWindow,
|
||||
'ZENITH_SIGNED_MESSAGE',
|
||||
SIGN_MESSAGE_RESPONSE,
|
||||
{ signature },
|
||||
origin,
|
||||
);
|
||||
@ -63,7 +77,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
console.error('Signing failed:', err);
|
||||
sendMessage(
|
||||
sourceWindow!,
|
||||
'ZENITH_SIGNED_MESSAGE',
|
||||
SIGN_MESSAGE_RESPONSE,
|
||||
{ error: err },
|
||||
origin,
|
||||
);
|
||||
@ -76,7 +90,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
if (sourceWindow && origin) {
|
||||
sendMessage(
|
||||
sourceWindow,
|
||||
'ZENITH_SIGNED_MESSAGE',
|
||||
SIGN_MESSAGE_RESPONSE,
|
||||
{ error: 'User rejected the request' },
|
||||
origin,
|
||||
);
|
||||
@ -85,7 +99,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleCosmosSignMessage = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'SIGN_ZENITH_MESSAGE') return;
|
||||
if (event.data.type !== REQUEST_SIGN_MESSAGE) return;
|
||||
|
||||
|
||||
if (!REACT_APP_ALLOWED_URLS) {
|
||||
@ -103,16 +117,25 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
|
||||
try {
|
||||
const { signerAddress, signDoc } = event.data.params;
|
||||
|
||||
const receivedNamespace = event.data.chainId.split(':')[0]
|
||||
const receivedChainId = event.data.chainId.split(':')[1]
|
||||
|
||||
if (receivedNamespace !== COSMOS) {
|
||||
// TODO: Support ETH namespace
|
||||
throw new Error(`namespace ${receivedNamespace} is not supported`)
|
||||
}
|
||||
|
||||
setSignerAddress(signerAddress);
|
||||
setSignDoc(signDoc);
|
||||
setMessage(signDoc.memo || '');
|
||||
setOrigin(event.origin);
|
||||
setSourceWindow(event.source as Window);
|
||||
setChainId(event.data.chainId);
|
||||
setNamespace(receivedNamespace);
|
||||
setChainId(receivedChainId);
|
||||
|
||||
const requestAccount = await retrieveSingleAccount(
|
||||
COSMOS,
|
||||
event.data.chainId,
|
||||
receivedNamespace,
|
||||
receivedChainId,
|
||||
signerAddress,
|
||||
);
|
||||
|
||||
|
392
src/screens/SignTxEmbed.tsx
Normal file
392
src/screens/SignTxEmbed.tsx
Normal file
@ -0,0 +1,392 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Text,
|
||||
} from 'react-native-paper';
|
||||
import JSONbig from 'json-bigint';
|
||||
import { AuthInfo, SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
|
||||
|
||||
import { DirectSecp256k1Wallet, Algo, TxBodyEncodeObject, decodeOptionalPubkey } from '@cosmjs/proto-signing';
|
||||
import { SigningStargateClient } from '@cosmjs/stargate';
|
||||
import { toHex } from '@cosmjs/encoding';
|
||||
|
||||
import { getCosmosAccountByHDPath, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { getMnemonic, getPathKey, sendMessage } from '../utils/misc';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import TxErrorDialog from '../components/TxErrorDialog';
|
||||
import { Account, NetworksDataState } from '../types';
|
||||
import { REQUEST_SIGN_TX, REQUEST_COSMOS_ACCOUNTS, COSMOS_ACCOUNTS_RESPONSE, SIGN_TX_RESPONSE } from '../utils/constants';
|
||||
|
||||
// Type Definitions
|
||||
interface GetAccountsRequestData {
|
||||
chainId: string,
|
||||
}
|
||||
|
||||
interface SignTxRequestData {
|
||||
address: string;
|
||||
signDoc: SignDoc;
|
||||
txBody: TxBodyEncodeObject;
|
||||
}
|
||||
|
||||
type IncomingMessageData = SignTxRequestData | GetAccountsRequestData;
|
||||
|
||||
interface IncomingMessageEventData {
|
||||
id: string;
|
||||
type: typeof REQUEST_SIGN_TX | typeof REQUEST_COSMOS_ACCOUNTS;
|
||||
data: IncomingMessageData;
|
||||
}
|
||||
|
||||
type TransactionDetails = {
|
||||
source: MessageEventSource;
|
||||
origin: string;
|
||||
signerAddress: string;
|
||||
chainId: string;
|
||||
account: Account;
|
||||
requestedNetwork: NetworksDataState;
|
||||
balance: string;
|
||||
signDoc: SignDoc;
|
||||
txBody: TxBodyEncodeObject;
|
||||
};
|
||||
|
||||
interface GetAccountsResponse {
|
||||
accounts: Array<{
|
||||
algo: Algo;
|
||||
address: string;
|
||||
pubkey: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
|
||||
|
||||
export const SignTxEmbed = () => {
|
||||
const [isTxApprovalVisible, setIsTxApprovalVisible] = useState<boolean>(false);
|
||||
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
|
||||
const [isTxLoading, setIsTxLoading] = useState(false);
|
||||
const [txError, setTxError] = useState<string | null>(null);
|
||||
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
// Message Handlers
|
||||
|
||||
const handleGetCosmosAccountsRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
|
||||
const { data } = event.data;
|
||||
const source = event.source as Window;
|
||||
const origin = event.origin;
|
||||
const requestData = data as GetAccountsRequestData;
|
||||
const mnemonic = await getMnemonic();
|
||||
|
||||
try {
|
||||
const requestedNetworkData = networksData.find(networkData => networkData.chainId === requestData.chainId)
|
||||
|
||||
if(!requestedNetworkData) {
|
||||
throw new Error("Network data not found")
|
||||
}
|
||||
|
||||
const allAccounts = await retrieveAccounts(requestedNetworkData);
|
||||
|
||||
if (!allAccounts || allAccounts.length === 0) {
|
||||
throw new Error("Accounts not found for network")
|
||||
}
|
||||
|
||||
const responseAccounts = await Promise.all(
|
||||
allAccounts.map(async (acc) => {
|
||||
const cosmosAccount = (await getCosmosAccountByHDPath(mnemonic, acc.hdPath, requestedNetworkData.addressPrefix)).data;
|
||||
return {
|
||||
...cosmosAccount,
|
||||
pubkey: toHex(cosmosAccount.pubkey),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const response: GetAccountsResponse = { accounts: responseAccounts };
|
||||
sendMessage(source, COSMOS_ACCOUNTS_RESPONSE, {data: response}, origin);
|
||||
} catch (error: unknown) {
|
||||
console.error(`Error handling ${REQUEST_COSMOS_ACCOUNTS}:`, error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
sendMessage(source, COSMOS_ACCOUNTS_RESPONSE, { error: `Failed to get accounts: ${errorMsg}` }, origin);
|
||||
}
|
||||
}, [networksData]);
|
||||
|
||||
const handleSignTxRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
|
||||
const { data } = event.data;
|
||||
const source = event.source as Window;
|
||||
const origin = event.origin;
|
||||
const requestData = data as SignTxRequestData;
|
||||
|
||||
setIsTxApprovalVisible(false);
|
||||
setTransactionDetails(null);
|
||||
setTxError(null);
|
||||
|
||||
try {
|
||||
const { address: signerAddress, signDoc, txBody } = requestData;
|
||||
|
||||
const network = networksData.find(net => net.chainId === signDoc.chainId);
|
||||
if (!network) throw new Error(`Network with chainId "${signDoc.chainId}" not supported.`);
|
||||
|
||||
const account = await retrieveSingleAccount(network.namespace, network.chainId, signerAddress);
|
||||
if (!account) throw new Error(`Account not found for address "${signerAddress}" on chain "${signDoc.chainId}".`);
|
||||
|
||||
// Balance Check
|
||||
// Use a temporary read-only client for balance
|
||||
const { privKey } = await getPathKey(`${network.namespace}:${network.chainId}`, account.index)
|
||||
|
||||
const tempWallet = await DirectSecp256k1Wallet.fromKey(
|
||||
new Uint8Array(Buffer.from(privKey.replace(/^0x/, ''), 'hex')),
|
||||
network.addressPrefix
|
||||
);
|
||||
|
||||
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, tempWallet);
|
||||
const balance = await client.getBalance(account.address, network.nativeDenom!);
|
||||
client.disconnect();
|
||||
|
||||
if (!balance || balance.amount === "0") {
|
||||
throw new Error(`${account.address} does not have any balance`)
|
||||
}
|
||||
|
||||
setTransactionDetails({
|
||||
source: source,
|
||||
origin: origin,
|
||||
signerAddress,
|
||||
chainId: signDoc.chainId,
|
||||
account,
|
||||
requestedNetwork: network,
|
||||
balance: balance.amount,
|
||||
signDoc,
|
||||
txBody
|
||||
});
|
||||
|
||||
setIsTxApprovalVisible(true);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error(`Error handling ${REQUEST_SIGN_TX}:`, error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
sendMessage(source, SIGN_TX_RESPONSE, { error: `Failed to prepare transaction: ${errorMsg}` }, origin);
|
||||
setTxError(errorMsg);
|
||||
}
|
||||
}, [networksData]);
|
||||
|
||||
const handleIncomingMessage = useCallback((event: MessageEvent) => {
|
||||
if (!event.data || typeof event.data !== 'object' || !event.data.type || !event.source || event.source === window) {
|
||||
return; // Basic validation
|
||||
}
|
||||
|
||||
if (!REACT_APP_ALLOWED_URLS) {
|
||||
console.log('Allowed URLs are not set');
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedUrls = REACT_APP_ALLOWED_URLS.split(',').map(url => url.trim());
|
||||
|
||||
if (!allowedUrls.includes(event.origin)) {
|
||||
console.log('Unauthorized app.');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageData = event.data as IncomingMessageEventData;
|
||||
|
||||
switch (messageData.type) {
|
||||
case REQUEST_COSMOS_ACCOUNTS:
|
||||
handleGetCosmosAccountsRequest(event as MessageEvent<IncomingMessageEventData>);
|
||||
break;
|
||||
case REQUEST_SIGN_TX:
|
||||
handleSignTxRequest(event as MessageEvent<IncomingMessageEventData>);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Received unknown message type: ${messageData.type}`);
|
||||
}
|
||||
}, [handleGetCosmosAccountsRequest, handleSignTxRequest]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleIncomingMessage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleIncomingMessage);
|
||||
};
|
||||
}, [handleIncomingMessage]);
|
||||
|
||||
// Action Handlers
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
if (!transactionDetails) {
|
||||
setTxError("Transaction details are missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTxLoading(true);
|
||||
setTxError(null);
|
||||
|
||||
const { source, origin, requestedNetwork, chainId, account, signerAddress, signDoc } = transactionDetails;
|
||||
|
||||
try {
|
||||
const { privKey } = await getPathKey(`${requestedNetwork.namespace}:${chainId}`, account.index);
|
||||
|
||||
const privateKeyBytes = Buffer.from(privKey.replace(/^0x/, ''), 'hex');
|
||||
const wallet = await DirectSecp256k1Wallet.fromKey(new Uint8Array(privateKeyBytes), requestedNetwork.addressPrefix); // Wrap in Uint8Array
|
||||
|
||||
// Perform the actual signing
|
||||
const signResponse = await wallet.signDirect(signerAddress, signDoc);
|
||||
|
||||
sendMessage(source as Window, SIGN_TX_RESPONSE, {data: signResponse}, origin);
|
||||
|
||||
setIsTxApprovalVisible(false);
|
||||
setTransactionDetails(null);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Error during signDirect:", error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
setTxError(errorMsg);
|
||||
sendMessage(source as Window, SIGN_TX_RESPONSE, { error: `Failed to sign transaction: ${errorMsg}` }, origin);
|
||||
} finally {
|
||||
setIsTxLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rejectRequestHandler = () => {
|
||||
if (!transactionDetails) return;
|
||||
const { source, origin } = transactionDetails;
|
||||
|
||||
sendMessage(source as Window, SIGN_TX_RESPONSE, { error: "User rejected the signature request." }, origin);
|
||||
setIsTxApprovalVisible(false);
|
||||
setTransactionDetails(null);
|
||||
setTxError(null);
|
||||
};
|
||||
|
||||
const decodedAuth = React.useMemo(() => {
|
||||
if (!transactionDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
const info = AuthInfo.decode(transactionDetails.signDoc.authInfoBytes);
|
||||
return {
|
||||
...info,
|
||||
signerInfos: info.signerInfos.map((signerInfo) => ({
|
||||
...signerInfo,
|
||||
publicKey: decodeOptionalPubkey(signerInfo.publicKey),
|
||||
})),
|
||||
};
|
||||
}, [transactionDetails]);
|
||||
|
||||
const formattedTxBody = React.useMemo(
|
||||
() => {
|
||||
if (!transactionDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
return JSONbig.stringify(transactionDetails.txBody, null, 2)
|
||||
},
|
||||
[transactionDetails]
|
||||
);
|
||||
|
||||
const formattedAuthInfo = React.useMemo(
|
||||
() => JSONbig.stringify(decodedAuth, null, 2),
|
||||
[decodedAuth]
|
||||
);
|
||||
|
||||
const formattedSignDoc = React.useMemo(
|
||||
() =>
|
||||
{
|
||||
if (!transactionDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
return JSONbig.stringify(
|
||||
{
|
||||
...transactionDetails.signDoc,
|
||||
bodyBytes: toHex(transactionDetails.signDoc.bodyBytes),
|
||||
authInfoBytes: toHex(transactionDetails.signDoc.authInfoBytes),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
},
|
||||
[transactionDetails]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTxApprovalVisible && transactionDetails ? (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text variant='titleLarge'>Account</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<AccountDetails account={transactionDetails.account} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text variant='titleLarge'>{`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<Text variant='bodyLarge'>
|
||||
{transactionDetails.balance}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text variant='titleLarge'>Transaction Body</Text>
|
||||
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
|
||||
<Text style={styles.codeText}>
|
||||
{formattedTxBody}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text variant='titleLarge'>Auth Info</Text>
|
||||
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
|
||||
<Text style={styles.codeText}>
|
||||
{formattedAuthInfo}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text variant='titleLarge'>Transaction Data To Be Signed</Text>
|
||||
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
|
||||
<Text style={styles.codeText}>
|
||||
{formattedSignDoc}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
disabled={isTxLoading}
|
||||
style={{marginTop: 10}}
|
||||
>
|
||||
{isTxLoading ? 'Processing...' : 'Approve'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D"
|
||||
disabled={isTxLoading}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<Text style={{ marginTop: 50, textAlign: 'center' }}>Waiting for request...</Text>
|
||||
</View>
|
||||
)}
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={!!txError}
|
||||
hideDialog={() => setTxError(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -26,6 +26,7 @@ import { MEMO } from '../screens/ApproveTransfer';
|
||||
import { Account, NetworksDataState } from '../types';
|
||||
import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts';
|
||||
import useAccountsData from '../hooks/useAccountsData';
|
||||
import { REQUEST_TX, REQUEST_WALLET_ACCOUNTS, TRANSACTION_RESPONSE, WALLET_ACCOUNTS_DATA } from '../utils/constants';
|
||||
|
||||
type TransactionDetails = {
|
||||
chainId: string;
|
||||
@ -51,7 +52,7 @@ export const WalletEmbed = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleGetAccounts = async (event: MessageEvent) => {
|
||||
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
|
||||
if (event.data.type !== REQUEST_WALLET_ACCOUNTS) return;
|
||||
|
||||
const accountsData = await getAccountsData(event.data.chainId);
|
||||
|
||||
@ -62,7 +63,7 @@ export const WalletEmbed = () => {
|
||||
|
||||
sendMessage(
|
||||
event.source as Window,
|
||||
'WALLET_ACCOUNTS_DATA',
|
||||
WALLET_ACCOUNTS_DATA,
|
||||
accountsData.map(account => account.address),
|
||||
event.origin
|
||||
);
|
||||
@ -81,7 +82,7 @@ export const WalletEmbed = () => {
|
||||
const handleTxRequested = useCallback(
|
||||
async (event: MessageEvent) => {
|
||||
try {
|
||||
if (event.data.type !== 'REQUEST_TX') return;
|
||||
if (event.data.type !== REQUEST_TX) return;
|
||||
|
||||
txEventRef.current = event;
|
||||
|
||||
@ -139,7 +140,6 @@ export const WalletEmbed = () => {
|
||||
});
|
||||
|
||||
if (!checkSufficientFunds(amount, balance.amount)) {
|
||||
console.log("Insufficient funds detected. Throwing error.");
|
||||
throw new Error('Insufficient funds');
|
||||
}
|
||||
|
||||
@ -207,7 +207,7 @@ export const WalletEmbed = () => {
|
||||
|
||||
const event = txEventRef.current;
|
||||
if (event?.source) {
|
||||
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', txResult.transactionHash, event.origin);
|
||||
sendMessage(event.source as Window, TRANSACTION_RESPONSE, txResult.transactionHash, event.origin);
|
||||
} else {
|
||||
console.error('No event source available to send message');
|
||||
}
|
||||
@ -227,7 +227,7 @@ export const WalletEmbed = () => {
|
||||
setIsTxRequested(false);
|
||||
setTransactionDetails(null);
|
||||
if (event?.source) {
|
||||
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', null, event.origin);
|
||||
sendMessage(event.source as Window, TRANSACTION_RESPONSE, null, event.origin);
|
||||
} else {
|
||||
console.error('No event source available to send message');
|
||||
}
|
||||
@ -307,7 +307,7 @@ export const WalletEmbed = () => {
|
||||
hideDialog={() => {
|
||||
setTxError(null)
|
||||
if (window.parent) {
|
||||
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
|
||||
sendMessage(window.parent, TRANSACTION_RESPONSE, null, '*');
|
||||
sendMessage(window.parent, 'closeIframe', null, '*');
|
||||
}
|
||||
}}
|
||||
|
@ -355,6 +355,10 @@ const styles = StyleSheet.create({
|
||||
marginTop: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
codeText: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
|
@ -40,7 +40,8 @@ export type StackParamsList = {
|
||||
};
|
||||
"wallet-embed": undefined;
|
||||
"auto-sign-in": undefined;
|
||||
"sign-request-embed": undefined;
|
||||
"sign-message-request-embed": undefined;
|
||||
"sign-tx-request-embed": undefined;
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
|
@ -56,7 +56,7 @@ const createWalletFromMnemonic = async (
|
||||
|
||||
case COSMOS:
|
||||
address = (
|
||||
await getCosmosAccounts(mnemonic, hdPath, network.addressPrefix)
|
||||
await getCosmosAccountByHDPath(mnemonic, hdPath, network.addressPrefix)
|
||||
).data.address;
|
||||
break;
|
||||
|
||||
@ -281,7 +281,7 @@ const accountInfoFromHDPath = async (
|
||||
break;
|
||||
case COSMOS:
|
||||
address = (
|
||||
await getCosmosAccounts(mnemonic, hdPath, networkData.addressPrefix)
|
||||
await getCosmosAccountByHDPath(mnemonic, hdPath, networkData.addressPrefix)
|
||||
).data.address;
|
||||
break;
|
||||
default:
|
||||
@ -323,7 +323,7 @@ const updateAccountCounter = async (
|
||||
);
|
||||
};
|
||||
|
||||
const getCosmosAccounts = async (
|
||||
const getCosmosAccountByHDPath = async (
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
prefix: string = COSMOS,
|
||||
@ -351,5 +351,5 @@ export {
|
||||
accountInfoFromHDPath,
|
||||
getNextAccountId,
|
||||
updateAccountCounter,
|
||||
getCosmosAccounts,
|
||||
getCosmosAccountByHDPath,
|
||||
};
|
||||
|
@ -74,3 +74,26 @@ export const INVALID_URL_ERROR = 'Invalid URL';
|
||||
export const IS_NUMBER_REGEX = /^\d+$/;
|
||||
|
||||
export const IS_IMPORT_WALLET_ENABLED = false;
|
||||
|
||||
// iframe request types
|
||||
export const REQUEST_COSMOS_ACCOUNTS = 'REQUEST_COSMOS_ACCOUNTS';
|
||||
export const REQUEST_SIGN_TX = 'REQUEST_SIGN_TX';
|
||||
export const REQUEST_SIGN_MESSAGE = 'REQUEST_SIGN_MESSAGE';
|
||||
export const REQUEST_WALLET_ACCOUNTS = 'REQUEST_WALLET_ACCOUNTS';
|
||||
export const REQUEST_CREATE_OR_GET_ACCOUNTS = 'REQUEST_CREATE_OR_GET_ACCOUNTS';
|
||||
export const REQUEST_TX = 'REQUEST_TX';
|
||||
export const REQUEST_ACCOUNT_PK = 'REQUEST_ACCOUNT_PK';
|
||||
export const REQUEST_ADD_ACCOUNT = 'REQUEST_ADD_ACCOUNT';
|
||||
export const AUTO_SIGN_IN = 'AUTO_SIGN_IN';
|
||||
export const CHECK_BALANCE = 'CHECK_BALANCE';
|
||||
|
||||
// iframe response types
|
||||
export const COSMOS_ACCOUNTS_RESPONSE = 'COSMOS_ACCOUNTS_RESPONSE';
|
||||
export const SIGN_TX_RESPONSE = 'SIGN_TX_RESPONSE';
|
||||
export const SIGN_MESSAGE_RESPONSE = 'SIGN_MESSAGE_RESPONSE';
|
||||
export const TRANSACTION_RESPONSE = 'TRANSACTION_RESPONSE';
|
||||
export const SIGN_IN_RESPONSE = 'SIGN_IN_RESPONSE';
|
||||
export const ACCOUNT_PK_RESPONSE = 'ACCOUNT_PK_RESPONSE';
|
||||
export const ADD_ACCOUNT_RESPONSE = 'ADD_ACCOUNT_RESPONSE';
|
||||
export const WALLET_ACCOUNTS_DATA = 'WALLET_ACCOUNTS_DATA';
|
||||
export const IS_SUFFICIENT = 'IS_SUFFICIENT';
|
||||
|
@ -10,7 +10,7 @@ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
|
||||
|
||||
import { SignMessageParams } from '../types';
|
||||
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
|
||||
import { getCosmosAccounts } from './accounts';
|
||||
import { getCosmosAccountByHDPath } from './accounts';
|
||||
import { COSMOS, EIP155 } from './constants';
|
||||
|
||||
const signMessage = async ({
|
||||
@ -58,7 +58,7 @@ const signCosmosMessage = async (
|
||||
const mnemonic = await getMnemonic();
|
||||
const addressPrefix = fromBech32(cosmosAddress).prefix
|
||||
|
||||
const cosmosAccount = await getCosmosAccounts(mnemonic, path, addressPrefix);
|
||||
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, addressPrefix);
|
||||
const address = cosmosAccount.data.address;
|
||||
const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino(
|
||||
address,
|
||||
|
@ -18,7 +18,7 @@ import { EIP155_SIGNING_METHODS } from './EIP155Data';
|
||||
import { signDirectMessage, signEthMessage } from '../sign-message';
|
||||
import { Account } from '../../types';
|
||||
import { getMnemonic, getPathKey } from '../misc';
|
||||
import { getCosmosAccounts } from '../accounts';
|
||||
import { getCosmosAccountByHDPath } from '../accounts';
|
||||
import { COSMOS_METHODS } from './COSMOSData';
|
||||
import { COSMOS } from '../constants';
|
||||
|
||||
@ -88,7 +88,7 @@ export async function approveWalletConnectRequest(
|
||||
addressPrefix = fromBech32(account.address).prefix
|
||||
}
|
||||
|
||||
const cosmosAccount = await getCosmosAccounts(mnemonic, path, addressPrefix);
|
||||
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, addressPrefix);
|
||||
const address = account.address;
|
||||
|
||||
switch (request.method) {
|
||||
|
26
yarn.lock
26
yarn.lock
@ -1410,6 +1410,15 @@
|
||||
bech32 "^1.1.4"
|
||||
readonly-date "^1.0.0"
|
||||
|
||||
"@cosmjs/encoding@^0.33.1":
|
||||
version "0.33.1"
|
||||
resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.33.1.tgz#77d6a8e0152c658ecf07b5aee3f5968d9071da50"
|
||||
integrity sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==
|
||||
dependencies:
|
||||
base64-js "^1.3.0"
|
||||
bech32 "^1.1.4"
|
||||
readonly-date "^1.0.0"
|
||||
|
||||
"@cosmjs/json-rpc@^0.32.4":
|
||||
version "0.32.4"
|
||||
resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.32.4.tgz#be91eb89ea78bd5dc02d0a9fa184dd6790790f0b"
|
||||
@ -3939,6 +3948,11 @@
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
|
||||
"@types/json-bigint@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.4.tgz#250d29e593375499d8ba6efaab22d094c3199ef3"
|
||||
integrity sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==
|
||||
|
||||
"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
@ -5375,6 +5389,11 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@^9.0.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd"
|
||||
integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
@ -9822,6 +9841,13 @@ jsesc@~0.5.0:
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
|
||||
|
||||
json-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
|
||||
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
|
||||
dependencies:
|
||||
bignumber.js "^9.0.0"
|
||||
|
||||
json-buffer@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||
|
Loading…
Reference in New Issue
Block a user