moved old docs -> old_docs dir for clarity when devving

This commit is contained in:
mfahampshire
2024-08-28 12:29:01 +02:00
parent 6f5231421b
commit ab1cf1e67d
589 changed files with 14281 additions and 46 deletions
+5 -13
View File
@@ -1,14 +1,6 @@
# mdbook files
book/
# Compiled assets
.sass-cache
_site
# Developing
todo.md
.idea
# OSX
.DS_Store
.next
node_modules
out
# the lock file will break Vercel because it may get committed from a machine with a different build architecture
package-lock.json
+15 -33
View File
@@ -1,42 +1,24 @@
# Nym Documentation
Documentation for the Nym privacy platform built using the [mdBook](https://rust-lang.github.io/mdBook/) docs framework.
# Nym Docs v2
Documentation can be viewed at https://nymtech.net/docs
New consolidated version of the nym docs.
## Contributing
Contributions to our documentation are very welcome. Please work on your contribution in either a `feature/<feature-name>` or `chore/<chore-name>` branch from `master` and target your pull request at `master`.
## Local development
Since these docs autogenerate command output and import docs from binaries in `target/release` on `build` make sure you're branching off of `master` when making your branch.
```
npm i
npm run dev
```
Changes merged to `master` will be autodeployed to the production site.
Open http://localhost:3000 to browse the output that will hot-reload when you make changes.
### Contributing a new translation
To contribute tranlsations in a new language, please get in touch via [Matrix](https://matrix.to/#/#general:nymtech.chat) or [Discord](https://nymtech.net/go/discord).
## Build
### Variables
There are some variables that are shared across the entire docs site, such as the current latest software version.
```
npm run build
```
Variables are denoted in the `.md` files wrapped in `{{}}` (e.g `{{wallet_release_version}}`), and are located in the `book.toml` file under the `[preprocessor.variables.variables]` heading. If you are changing something like the software release version, minimum code versions in prerequisites, etc, **check in here first!**
The static output will be in `./out`;
### Diagrams
Most diagrams are simply ascii. Copies are kept in `/diagrams/` for ease of reproducability. Created using [textik](https://textik.com/#).
## Template details
### Importing files and auto-generated command output
Example files are inserted as per normal with mdbook.
Some binary command outputs are generated using the [`cmdrun`](https://docs.rs/mdbook-cmdrun/latest/mdbook_cmdrun/) mdbook plugin.
## Building
When working locally, it is recommended that you use `mdbook serve` to have a local version of the docs served on `localhost:3000`, with hot reloading on any changes made to files in the `src/` directory.
You can find other commands in the [mdBook CLI tool docs](https://rust-lang.github.io/mdBook/cli/index.html).
### I tried to edit files in `theme/` and they aren't taking effect / `mdbook serve` causes a looping reload on file changes after changing fields in `[preprocessor.theme]` config
Looping reload is a known issue with the `mdbook-theme` preprocessor used for the table of contents and layout of these docs. As outlined in the `mdbook-theme` [readme](https://github.com/zjp-CN/mdbook-theme#avoid-repeating-call-on-this-tool-when-mdbook-watch) one way to mitigate this is to set `turn-off = true` under `[preprocessor.theme]`. This means that `mdbook serve` or `mdbook watch` ignores changes to the `theme/` directory, which is the source of the looping reload. If you have changed or commented out this line, reintroduce it to remove the looping reload. If you are trying to edit the theme of the docs and want to apply the change, see [here](https://github.com/zjp-CN/mdbook-theme#avoid-repeating-call-on-this-tool-when-mdbook-watch) for more info on how to remove the block, change the theme, and reintroduce the block.
### Checking the mdBook version
To check the version of mdBook installed on your system, you can use the `mdbook --version` command. This will print the version number of mdBook installed on your system in the terminal.
The latest release of the binary of the pre-compiled binaries can be found on [GitHub](https://github.com/rust-lang/mdBook/releases).
This documentation was made with [Nextra](https://nextra.site) using the template from here https://github.com/shuding/nextra-docs-template.
@@ -0,0 +1,190 @@
```ts copy filename="CosmosKitExample.tsx"
import React, { FC } from 'react';
import { ChainProvider, useChain } from '@cosmos-kit/react';
import { assets, chains } from 'chain-registry';
import { wallets as keplr } from '@cosmos-kit/keplr';
import { wallets as ledger } from '@cosmos-kit/ledger';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { Alert, AlertTitle } from '@mui/material';
import { Wallet } from '@cosmos-kit/core';
import { CosmosKitLedger } from './ledger';
import { CosmosKitSign } from './sign';
const CosmosKitSetup: FC<{ children: React.ReactNode }> = ({ children }) => {
const assetsFixedUp = React.useMemo(() => {
const nyx = assets.find((a) => a.chain_name === 'nyx');
if (nyx) {
const nyxCoin = nyx.assets.find((a) => a.name === 'nyx');
if (nyxCoin) {
nyxCoin.coingecko_id = 'nyx';
}
nyx.assets = nyx.assets.reverse();
}
return assets;
}, [assets]);
const chainsFixedUp = React.useMemo(() => {
const nyx = chains.find((c) => c.chain_id === 'nyx');
if (nyx) {
if (!nyx.staking) {
nyx.staking = {
staking_tokens: [{ denom: 'unyx' }],
lock_duration: {
blocks: 10000,
},
};
}
}
return chains;
}, [chains]);
return (
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...ledger, ...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
{children}
</ChainProvider>
);
};
function walletRejectMessageOrError(wallet?: Wallet, error?: React.ReactNode) {
if (!wallet) {
if (!error) {
return undefined;
}
return error;
}
if (typeof wallet.rejectMessage === 'string') {
return wallet.rejectMessage;
}
return wallet.rejectMessage.source;
}
const Wrapper: FC<{
children?: React.ReactNode;
wallet?: Wallet;
header?: React.ReactNode;
error?: React.ReactNode;
disconnect: () => Promise<void>;
}> = ({ wallet, disconnect, error, header, children }) => {
if (error) {
return (
<Box mt={4} mb={2}>
<Alert severity="error">
{wallet && <AlertTitle>Failed to connect to {wallet.prettyName}</AlertTitle>}
{wallet && walletRejectMessageOrError(wallet)}
</Alert>
<Box mt={2}>
<Button variant="outlined" onClick={disconnect}>
Retry
</Button>
</Box>
</Box>
);
}
return (
<Box mt={4} mb={2}>
<Box display="flex" justifyContent="space-between">
{header && header}
<Button variant="outlined" onClick={disconnect}>
Disconnect
</Button>
</Box>
{children}
</Box>
);
};
const CosmosKitInner = () => {
const {
wallet,
address,
connect,
disconnect,
isWalletConnecting,
isWalletDisconnected,
isWalletError,
isWalletNotExist,
isWalletRejected,
} = useChain('nyx');
if (isWalletError) {
return <Wrapper wallet={wallet} error="Oh no! Something went wrong." disconnect={disconnect} />;
}
if (isWalletNotExist) {
return <Wrapper wallet={wallet} error="Oh no! Could not connect to that wallet." disconnect={disconnect} />;
}
if (isWalletRejected) {
return (
<Wrapper
wallet={wallet}
error="Oh no! Did you authorize the connection to your wallet?"
disconnect={disconnect}
/>
);
}
if (isWalletDisconnected) {
return (
<Button variant="outlined" onClick={connect} sx={{ mt: 4 }}>
Connect your wallet
</Button>
);
}
if (isWalletConnecting) {
return <CircularProgress />;
}
// Ledger Hardware Wallet
if (wallet.mode === 'ledger') {
return (
<Wrapper
header={
<Box>
<strong>Connected to {wallet.prettyName}</strong>
<Typography>
Address: <code>{address}</code>{' '}
</Typography>
</Box>
}
disconnect={disconnect}
>
<CosmosKitLedger />
</Wrapper>
);
}
// Extension or Wallet Connect
return (
<Wrapper
header={
<Box>
<strong>Connected to {wallet.prettyName}</strong>
<Typography>
Address: <code>{address}</code>{' '}
</Typography>
</Box>
}
disconnect={disconnect}
>
<CosmosKitSign />
</Wrapper>
);
};
export const CosmosKit = () => (
<CosmosKitSetup>
<CosmosKitInner />
</CosmosKitSetup>
);
```
@@ -0,0 +1,84 @@
```ts copy filename="mixFetchExample.tsx"
import React, { useState } from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { mixFetch } from '@nymproject/mix-fetch-full-fat';
import Stack from '@mui/material/Stack';
import Paper from '@mui/material/Paper';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
const defaultUrl = 'https://nymtech.net/favicon.svg';
const args = { mode: 'unsafe-ignore-cors' };
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: '6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B', // with WSS
preferredNetworkRequester:
'8rRGWy54oC8drFL9DepMegBt2DLrsqQwCoHMXt9nsnTo.2XjCPVbb4FpQ9hNRcXwb9mTzEAVVk1zf1tcch3wdtNEA@6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B',
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra: {},
};
export const MixFetch = () => {
const [url, setUrl] = useState<string>(defaultUrl);
const [html, setHtml] = useState<string>();
const [busy, setBusy] = useState<boolean>(false);
const handleFetch = async () => {
try {
setBusy(true);
setHtml(undefined);
const response = await mixFetch(url, args, mixFetchOptions);
console.log(response);
const resHtml = await response.text();
setHtml(resHtml);
} catch (err) {
console.log(err);
} finally {
setBusy(false);
}
};
return (
<div style={{ marginTop: '1rem' }}>
<Stack direction="row">
<TextField
disabled={busy}
fullWidth
label="URL"
type="text"
variant="outlined"
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
/>
<Button variant="outlined" disabled={busy} sx={{ marginLeft: '1rem' }} onClick={handleFetch}>
Fetch
</Button>
</Stack>
{busy && (
<Box mt={4}>
<CircularProgress />
</Box>
)}
{html && (
<>
<Box mt={4}>
<strong>Response</strong>
</Box>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">
{html}
</Typography>
</Paper>
</>
)}
</div>
);
};
```
@@ -0,0 +1,54 @@
```ts copy filename="MixnodeContractQueryExample.ts"
import { useEffect, useState } from "react";
import { contracts } from "@nymproject/contract-clients";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { settings } from "./client";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
const getClient = async () => {
const cosmWasmClient = await SigningCosmWasmClient.connect(settings.url);
const client = new contracts.Mixnet.MixnetQueryClient(
cosmWasmClient,
settings.mixnetContractAddress
);
return client;
};
export const Mixnodes = () => {
const [mixnodes, setMixnodes] = useState<any>();
const getMixnodes = async () => {
const client = await getClient();
const { nodes } = await client.getMixNodesDetailed({});
setMixnodes(nodes);
};
useEffect(() => {
getMixnodes();
}, []);
if (!mixnodes) {
return (
<Box sx={{ display: "flex" }}>
<CircularProgress />
</Box>
);
}
return (
<div style={{ marginTop: "1rem" }}>
{mixnodes?.length &&
mixnodes.map((mixnode: any) => (
<Box className="codeBox" key={mixnode.bond_information.mix_id}>
<span
style={{ marginRight: "1rem" }}
>{`id: ${mixnode.bond_information.mix_id}`}</span>
<span>{`owner: ${mixnode.bond_information.owner}`}</span>
</Box>
))}
</div>
);
};
```
@@ -0,0 +1,103 @@
```ts copy filename="MixnetWASMClientExample.tsx"
import React, { useEffect, useState } from 'react';
import { createNymMixnetClient, NymMixnetClient, Payload } from '@nymproject/sdk-full-fat';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
const nymApiUrl = 'https://validator.nymtech.net/api';
export const Traffic = () => {
const [nym, setNym] = useState<NymMixnetClient>();
const [selfAddress, setSelfAddress] = useState<string>();
const [recipient, setRecipient] = useState<string>();
const [payload, setPayload] = useState<Payload>();
const [receivedMessage, setReceivedMessage] = useState<string>();
const init = async () => {
const client = await createNymMixnetClient();
setNym(client);
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
client?.events.subscribeToConnected((e) => {
const { address } = e.args;
setSelfAddress(address);
});
client?.events.subscribeToLoaded((e) => {
console.log('Client ready: ', e.args);
});
client?.events.subscribeToTextMessageReceivedEvent((e) => {
console.log(e.args.payload);
setReceivedMessage(e.args.payload);
});
};
const stop = async () => {
await nym?.client.stop();
};
const send = () => nym.client.send({ payload, recipient });
useEffect(() => {
init();
return () => {
stop();
};
}, []);
if (!nym || !selfAddress) {
return (
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
);
}
return (
<Box padding={3}>
<Paper style={{ marginTop: '1rem', padding: '2rem' }}>
<Stack spacing={3}>
<Typography variant="body1">My self address is:</Typography>
<Typography variant="body1">{selfAddress || 'loading'}</Typography>
<Typography variant="h5">Communication through the Mixnet</Typography>
<TextField
type="text"
placeholder="Recipient Address"
onChange={(e) => setRecipient(e.target.value)}
size="small"
/>
<TextField
type="text"
placeholder="Message to send"
multiline
rows={4}
onChange={(e) => setPayload({ message: e.target.value, mimeType: 'text/plain' })}
size="small"
/>
<Button variant="outlined" onClick={() => send()} disabled={!payload || !recipient} sx={{width: 'fit-content'}}>
Send
</Button>
</Stack>
{receivedMessage && (
<Stack spacing={3} style={{ marginTop: '1rem' }}>
<Typography variant="h5">Message Received!</Typography>
<Typography fontFamily="monospace">{receivedMessage}</Typography>
</Stack>
)}
</Paper>
</Box>
);
};
```
@@ -0,0 +1,133 @@
```ts copy filename="FormattedWalletConnectCode.tsx"
import React from 'react';
import { Coin } from '@cosmjs/stargate';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
// Connect method on Parent Component
const getSignerAccount = async () => {
setAccountLoading(true);
try {
const signer = await signerAccount(mnemonic);
const accounts = await signer.getAccounts();
if (accounts[0]) {
setAccount(accounts[0].address);
}
} catch (error) {
console.error(error);
}
setAccountLoading(false);
};
// Get Balance on Parent Component
const getBalance = useCallback(async () => {
setBalanceLoading(true);
try {
const newBalance = await signerCosmosWasmClient?.getBalance(account, 'unym');
setBalance(newBalance);
} catch (error) {
console.error(error);
}
setBalanceLoading(false);
}, [account, signerCosmosWasmClient]);
const getClients = async () => {
setClientLoading(true);
try {
setSignerCosmosWasmClient(await fetchSignerCosmosWasmClient(mnemonic));
setSignerClient(await fetchSignerClient(mnemonic));
} catch (error) {
console.error(error);
}
setClientLoading(false);
};
const connect = () => {
getSignerAccount();
getClients();
};
// Get Signner Account on Parent Component
const getSignerAccount = async () => {
setAccountLoading(true);
try {
const signer = await signerAccount(mnemonic);
const accounts = await signer.getAccounts();
if (accounts[0]) {
setAccount(accounts[0].address);
}
} catch (error) {
console.error(error);
}
setAccountLoading(false);
};
export const ConnectWallet = ({
setMnemonic,
connect,
mnemonic,
accountLoading,
clientLoading,
balanceLoading,
account,
balance,
connectButtonText,
}: {
setMnemonic: (value: string) => void;
connect: () => void;
mnemonic: string;
accountLoading: boolean;
clientLoading: boolean;
balanceLoading: boolean;
account: string;
balance: Coin;
connectButtonText: string;
}) => {
return (
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Typography variant="h5" textAlign="center">
Connect to your account
</Typography>
<Box padding={3}>
<Typography variant="h6">Your account</Typography>
<Box marginY={3}>
<Typography variant="body1" marginBottom={3}>
Enter the mnemonic
</Typography>
<TextField
type="text"
placeholder="mnemonic"
onChange={(e) => setMnemonic(e.target.value)}
fullWidth
multiline
maxRows={4}
sx={{ marginBottom: 3 }}
/>
<Button
variant="outlined"
onClick={() => connect()}
disabled={!mnemonic || accountLoading || clientLoading || balanceLoading}
>
{connectButtonText}
</Button>
</Box>
{account && balance ? (
<Box>
<Typography variant="body1">Address: {account}</Typography>
<Typography variant="body1">
Balance: {balance?.amount} {balance?.denom}
</Typography>
</Box>
) : (
<Box>
<Typography variant="body1">Please, enter your mnemonic to receive your account information</Typography>
</Box>
)}
</Box>
</Paper>
);
};
```
@@ -0,0 +1,189 @@
```ts copy filename="FormattedWalletDelegationsCode.tsx"
import React from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import { TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@mui/material';
import Table from '@mui/material/Table';
// Get Delegations on parent component
const getDelegations = useCallback(async () => {
const newDelegations = await signerClient.getDelegatorDelegations({
delegator: settings.address,
});
setDelegations(newDelegations);
}, [signerClient]);
// Make a Delegation on parent component
const doDelegate = async ({ mixId, amount }: { mixId: number; amount: number }) => {
if (!signerClient) {
return;
}
setDelegationLoader(true);
try {
const res = await signerClient.delegateToMixnode({ mixId }, 'auto', undefined, [
{ amount: `${amount}`, denom: 'unym' },
]);
console.log('res', res);
setLog((prev) => [
...prev,
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
]);
} catch (error) {
console.error(error);
}
setDelegationLoader(false);
};
// Undelegate All on Parent Component
const doUndelegateAll = async () => {
if (!signerClient) {
return;
}
setUndeledationLoader(true);
try {
// eslint-disable-next-line no-restricted-syntax
for (const delegation of delegations.delegations) {
// eslint-disable-next-line no-await-in-loop
await signerClient.undelegateFromMixnode({ mixId: delegation.mix_id }, 'auto');
}
} catch (error) {
console.error(error);
}
setUndeledationLoader(false);
};
// Withdraw Rewards on Parent Component
const doWithdrawRewards = async () => {
const delegatorAddress = '';
const validatorAdress = '';
const memo = 'test sending tokens';
setWithdrawLoading(true);
try {
const res = await signerCosmosWasmClient.withdrawRewards(delegatorAddress, validatorAdress, 'auto', memo);
setLog((prev) => [
...prev,
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
]);
} catch (error) {
console.error(error);
}
setWithdrawLoading(false);
};
import React, { useState } from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import { TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@mui/material';
import Table from '@mui/material/Table';
export const Delegations = ({
delegations,
doDelegate,
delegationLoader,
doUndelegateAll,
undeledationLoader,
doWithdrawRewards,
withdrawLoading,
}: {
delegations: any;
doDelegate: ({ mixId, amount }: { mixId: number; amount: number }) => void;
delegationLoader: boolean;
doUndelegateAll: () => void;
undeledationLoader: boolean;
doWithdrawRewards: () => void;
withdrawLoading: boolean;
}) => {
const [delegationNodeId, setDelegationNodeId] = useState<string>();
const [amountToBeDelegated, setAmountToBeDelegated] = useState<string>();
return (
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Box padding={3}>
<Typography variant="h6">Delegations</Typography>
<Box marginY={3}>
<Box marginY={3} display="flex" flexDirection="column">
<Typography marginBottom={3} variant="body1">
Make a delegation
</Typography>
<TextField
type="text"
placeholder="Mixnode ID"
onChange={(e) => setDelegationNodeId(e.target.value)}
size="small"
/>
<Box marginTop={3} display="flex" justifyContent="space-between">
<TextField
type="text"
placeholder="Amount"
onChange={(e) => setAmountToBeDelegated(e.target.value)}
size="small"
/>
<Button
variant="outlined"
onClick={() =>
doDelegate({ mixId: parseInt(delegationNodeId, 10), amount: parseInt(amountToBeDelegated, 10) })
}
disabled={delegationLoader}
>
{delegationLoader ? 'Delegation in process...' : 'Delegate'}
</Button>
</Box>
</Box>
</Box>
<Box marginTop={3}>
<Typography variant="body1">Your delegations</Typography>
<Box marginBottom={3} display="flex" flexDirection="column">
{!delegations?.delegations?.length ? (
<Typography>You do not have delegations</Typography>
) : (
<Box>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>MixId</TableCell>
<TableCell>Owner</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Cumulative Reward Ratio</TableCell>
</TableRow>
</TableHead>
<TableBody>
{delegations?.delegations.map((delegation: any) => (
<TableRow key={delegation.mix_id}>
<TableCell>{delegation.mix_id}</TableCell>
<TableCell>{delegation.owner}</TableCell>
<TableCell>{delegation.amount.amount}</TableCell>
<TableCell>{delegation.cumulative_reward_ratio}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
)}
</Box>
{delegations && (
<Box marginBottom={3}>
<Button variant="outlined" onClick={() => doUndelegateAll()} disabled={undeledationLoader}>
{undeledationLoader ? 'Undelegating...' : 'Undelegate All'}
</Button>
</Box>
)}
<Box>
<Button variant="outlined" onClick={() => doWithdrawRewards()} disabled={withdrawLoading}>
{withdrawLoading ? 'Doing withdraw...' : 'Withdraw rewards'}
</Button>
</Box>
</Box>
</Box>
</Paper>
);
};
```
@@ -0,0 +1,72 @@
```ts copy filename="FormattedWalletSendTokensCode.tsx"
import React, { useState } from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
// Send tokens on Parent component
const doSendTokens = async (amount: string) => {
const memo = 'test sending tokens';
setSendingTokensLoader(true);
try {
const res = await signerCosmosWasmClient.sendTokens(
account,
recipientAddress,
[{ amount, denom: 'unym' }],
'auto',
memo,
);
setLog((prev) => [
...prev,
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
]);
} catch (error) {
console.error(error);
}
setSendingTokensLoader(false);
};
export const SendTokes = ({
setRecipientAddress,
doSendTokens,
sendingTokensLoader,
}: {
setRecipientAddress: (value: string) => void;
doSendTokens: (amount: string) => void;
sendingTokensLoader: boolean;
}) => {
const [tokensToSend, setTokensToSend] = useState<string>();
return (
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Box padding={3}>
<Typography variant="h6">Send Tokens</Typography>
<Box marginTop={3} display="flex" flexDirection="column">
<TextField
type="text"
placeholder="Recipient Address"
onChange={(e) => setRecipientAddress(e.target.value)}
size="small"
/>
<Box marginY={3} display="flex" justifyContent="space-between">
<TextField
type="text"
placeholder="Amount"
onChange={(e) => setTokensToSend(e.target.value)}
size="small"
/>
<Button variant="outlined" onClick={() => doSendTokens(amount)} disabled={sendingTokensLoader}>
{sendingTokensLoader ? 'Sending...' : 'SendTokens'}
</Button>
</Box>
</Box>
</Box>
</Paper>
);
};
```
@@ -0,0 +1,47 @@
import React, { useState } from 'react';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
export const GitHubRepoSearch = () => {
const [repoUrl, setRepoUrl] = useState('');
const handleSearch = () => {
if(!repoUrl || repoUrl.length < 1 ) {
return window.alert("Please enter a valid Github URL!")
}
const matchedRepo = repoUrl.match(/https:\/\/github\.com\/(.*)/)[1]
// Construct the search URL
const searchUrl = `https://github.com/search?q=repo:${matchedRepo} fetch(&type=code`;
// Redirect the user to a new search results page
window.open(searchUrl, "_blank");
};
return (
<Box padding={3}>
<Box>
<TextField
type="text"
placeholder="Enter GitHub repo URL: https://github.com/nymtech/nym/"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
size="small"
sx={{width: "450px"}}
/>
<Button
variant="outlined"
onClick={handleSearch}
size="medium"
sx={{ marginLeft: 2, marginTop: 0.2 }}
>
Check mixFetch
</Button>
</Box>
</Box>
);
}
@@ -0,0 +1,15 @@
export const mainnetSettings = {
url: 'wss://rpc.nymtech.net:443',
mixnetContractAddress: 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr',
mnemonic: process.env.MAINNET_MNEMONIC,
address: 'n1c7y676pe3av76r5usala759xgj0yplmvngu8u8',
};
export const qaSettings = {
url: 'wss://rpc.sandbox.nymtech.net',
mixnetContractAddress: 'n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav',
mnemonic: process.env.QA_MNEMONIC,
address: 'n13uryxldwdllpakevsmt6n0uyfn3kgr2wvj5dnf',
};
export const settings = qaSettings;
@@ -0,0 +1,28 @@
import { AminoMsg, makeSignDoc, serializeSignDoc } from '@cosmjs/amino';
import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx';
export const getDoc = (address: string) => {
const chainId = 'nyx';
const msg: AminoMsg = {
type: '/cosmos.bank.v1beta1.MsgSend',
value: MsgSend.fromPartial({
fromAddress: address,
toAddress: 'n1nn8tghp94n8utsgyg3kfttlxm0exgjrsqkuwu9',
amount: [{ amount: '1000', denom: 'unym' }],
}),
};
const fee = {
amount: [{ amount: '2000', denom: 'ucosm' }],
gas: '180000', // 180k
};
const memo = 'Use your power wisely';
const accountNumber = 15;
const sequence = 16;
return makeSignDoc([msg], fee, chainId, memo, accountNumber, sequence);
};
export const aminoDoc = (address: string) => {
const signDoc = getDoc(address);
return serializeSignDoc(signDoc);
};
@@ -0,0 +1,188 @@
import React, { FC } from 'react';
import { ChainProvider, useChain } from '@cosmos-kit/react';
import { assets, chains } from 'chain-registry';
import { wallets as keplr } from '@cosmos-kit/keplr-extension';
import { wallets as ledger } from '@cosmos-kit/ledger';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { Alert, AlertTitle } from '@mui/material';
import { Wallet } from '@cosmos-kit/core';
import { CosmosKitLedger } from './ledger';
import { CosmosKitSign } from './sign';
const CosmosKitSetup: FC<{ children: React.ReactNode }> = ({ children }) => {
const assetsFixedUp = React.useMemo(() => {
const nyx = assets.find((a) => a.chain_name === 'nyx');
if (nyx) {
const nyxCoin = nyx.assets.find((a) => a.name === 'nyx');
if (nyxCoin) {
nyxCoin.coingecko_id = 'nyx';
}
nyx.assets = nyx.assets.reverse();
}
return assets;
}, [assets]);
const chainsFixedUp = React.useMemo(() => {
const nyx = chains.find((c) => c.chain_id === 'nyx');
if (nyx) {
if (!nyx.staking) {
nyx.staking = {
staking_tokens: [{ denom: 'unyx' }],
lock_duration: {
blocks: 10000,
},
};
}
}
return chains;
}, [chains]);
return (
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...ledger, ...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
{children}
</ChainProvider>
);
};
function walletRejectMessageOrError(wallet?: Wallet, error?: React.ReactNode) {
if (!wallet) {
if (!error) {
return undefined;
}
return error;
}
if (typeof wallet.rejectMessage === 'string') {
return wallet.rejectMessage;
}
return wallet.rejectMessage.source;
}
const Wrapper: FC<{
children?: React.ReactNode;
wallet?: Wallet;
header?: React.ReactNode;
error?: React.ReactNode;
disconnect: () => Promise<void>;
}> = ({ wallet, disconnect, error, header, children }) => {
if (error) {
return (
<Box mt={4} mb={2}>
<Alert severity="error">
{wallet && <AlertTitle>Failed to connect to {wallet.prettyName}</AlertTitle>}
{wallet && walletRejectMessageOrError(wallet)}
</Alert>
<Box mt={2}>
<Button variant="outlined" onClick={disconnect}>
Retry
</Button>
</Box>
</Box>
);
}
return (
<Box mt={4} mb={2}>
<Box display="flex" justifyContent="space-between">
{header && header}
<Button variant="outlined" onClick={disconnect}>
Disconnect
</Button>
</Box>
{children}
</Box>
);
};
const CosmosKitInner = () => {
const {
wallet,
address,
connect,
disconnect,
isWalletConnecting,
isWalletDisconnected,
isWalletError,
isWalletNotExist,
isWalletRejected,
} = useChain('nyx');
if (isWalletError) {
return <Wrapper wallet={wallet} error="Oh no! Something went wrong." disconnect={disconnect} />;
}
if (isWalletNotExist) {
return <Wrapper wallet={wallet} error="Oh no! Could not connect to that wallet." disconnect={disconnect} />;
}
if (isWalletRejected) {
return (
<Wrapper
wallet={wallet}
error="Oh no! Did you authorize the connection to your wallet?"
disconnect={disconnect}
/>
);
}
if (isWalletDisconnected) {
return (
<Button variant="outlined" onClick={connect} sx={{ mt: 4 }}>
Connect your wallet
</Button>
);
}
if (isWalletConnecting) {
return <CircularProgress />;
}
// Ledger Hardware Wallet
if (wallet.mode === 'ledger') {
return (
<Wrapper
header={
<Box>
<strong>Connected to {wallet.prettyName}</strong>
<Typography>
Address: <code>{address}</code>{' '}
</Typography>
</Box>
}
disconnect={disconnect}
>
<CosmosKitLedger />
</Wrapper>
);
}
// Extension or Wallet Connect
return (
<Wrapper
header={
<Box>
<strong>Connected to {wallet.prettyName}</strong>
<Typography>
Address: <code>{address}</code>{' '}
</Typography>
</Box>
}
disconnect={disconnect}
>
<CosmosKitSign />
</Wrapper>
);
};
export const CosmosKit = () => (
<CosmosKitSetup>
<CosmosKitInner />
</CosmosKitSetup>
);
@@ -0,0 +1,74 @@
import React from 'react';
import { useChain, useWalletClient } from '@cosmos-kit/react';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { pubkeyType } from '@cosmjs/amino';
import { toBase64 } from '@cosmjs/encoding';
import { Alert, AlertTitle, LinearProgress } from '@mui/material';
import { aminoDoc } from './data';
export const CosmosKitLedger = () => {
const { wallet, address } = useChain('nyx');
const { client } = useWalletClient(wallet?.name);
const [signResponse, setSignResponse] = React.useState<any>();
const [busy, setBusy] = React.useState<boolean>();
const sign = async () => {
setBusy(true);
const serialized = aminoDoc(address);
const account = await client.getAccount('nyx');
console.log('Accounts: ', account);
console.log('Info', await (client as any).client.getAppConfiguration());
const sigAmino = await (client as any).client.sign(account.username, serialized);
const sig = {
signature: toBase64(sigAmino.signature),
pub_key: {
type: pubkeyType.secp256k1,
value: toBase64(account.pubkey),
},
};
console.log('Sig', { sigAmino, sig });
setBusy(false);
return { signature: sig };
};
const handleSign = async () => {
setSignResponse(await sign());
};
if (busy) {
return (
<Box mt={4} mb={2}>
<LinearProgress color="success" />
<Alert severity="success">
<AlertTitle>Please approve on the Ledger</AlertTitle>
Follow the instructions on the Ledger to review the message
</Alert>
</Box>
);
}
return (
<>
{!signResponse && (
<Box mt={4} mb={2}>
<Box mb={2}>Click the button below to sign a fake request with your Ledger</Box>
<Button variant="outlined" onClick={handleSign}>
Click to sign
</Button>
</Box>
)}
{signResponse && (
<Box mt={2}>
<strong>Signature:</strong>
<Box sx={{ overflowX: 'auto' }}>
<pre>{JSON.stringify(signResponse?.signature, null, 2)}</pre>
</Box>
</Box>
)}
</>
);
};
@@ -0,0 +1,57 @@
import React from 'react';
import { useChain } from '@cosmos-kit/react';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { Alert, AlertTitle, LinearProgress } from '@mui/material';
import { getDoc } from './data';
export const CosmosKitSign = () => {
const { address, getOfflineSignerAmino } = useChain('nyx');
const [signResponse, setSignResponse] = React.useState<any>();
const [busy, setBusy] = React.useState<boolean>();
const sign = async () => {
setBusy(true);
const doc = getDoc(address);
const tx = await getOfflineSignerAmino().signAmino(address, doc);
setBusy(false);
return tx;
};
const handleSign = async () => {
setSignResponse(await sign());
};
if (busy) {
return (
<Box mt={4} mb={2}>
<LinearProgress color="success" />
<Alert severity="success">
<AlertTitle>Please approve in your wallet</AlertTitle>
Review the message to sign
</Alert>
</Box>
);
}
return (
<>
{!signResponse && (
<Box mt={4} mb={2}>
<Box mb={2}>Click the button below to sign a fake request with your wallet</Box>
<Button variant="outlined" onClick={handleSign}>
Click to sign
</Button>
</Box>
)}
{signResponse && (
<Box mt={2}>
<strong>Signature:</strong>
<Box sx={{ overflowX: 'auto' }}>
<pre>{JSON.stringify(signResponse?.signature, null, 2)}</pre>
</Box>
</Box>
)}
</>
);
};
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import Stack from '@mui/material/Stack';
const links = [
['Twitter', 'https://nymtech.net/go/twitter'],
['Telegram', 'https://nymtech.net/go/telegram'],
['Discord', 'https://nymtech.net/go/discord'],
['GitHub', 'https://nymtech.net/go/github/nym'],
['Nym Wallet', 'https://nymtech.net/download/wallet'],
['Nym Explorer', 'https://explorer.nymtech.net/'],
['Nym Blog', 'https://nymtech.medium.com/'],
['Nym Shipyard', 'https://shipyard.nymtech.net/'],
];
export const Footer = () => (
<Stack direction="row" spacing={3}>
{links.map((link) => (
<a key={link[1]} href={link[1]}>
{link[0]}
</a>
))}
</Stack>
);
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { mixFetch } from '@nymproject/mix-fetch-full-fat';
import Stack from '@mui/material/Stack';
import Paper from '@mui/material/Paper';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';
const defaultUrl = 'https://nymtech.net/favicon.svg';
const args = { mode: 'unsafe-ignore-cors' };
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: '6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B', // with WSS
preferredNetworkRequester:
'8rRGWy54oC8drFL9DepMegBt2DLrsqQwCoHMXt9nsnTo.2XjCPVbb4FpQ9hNRcXwb9mTzEAVVk1zf1tcch3wdtNEA@6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B',
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra: {},
};
export const MixFetch = () => {
const [url, setUrl] = useState<string>(defaultUrl);
const [html, setHtml] = useState<string>();
const [busy, setBusy] = useState<boolean>(false);
const handleFetch = async () => {
try {
setBusy(true);
setHtml(undefined);
const response = await mixFetch(url, args, mixFetchOptions);
console.log(response);
const resHtml = await response.text();
setHtml(resHtml);
} catch (err) {
console.log(err);
} finally {
setBusy(false);
}
};
return (
<div style={{ marginTop: '1rem' }}>
<Stack direction="row">
<TextField
disabled={busy}
fullWidth
label="URL"
type="text"
variant="outlined"
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
/>
<Button variant="outlined" disabled={busy} sx={{ marginLeft: '1rem' }} onClick={handleFetch}>
Fetch
</Button>
</Stack>
{busy && (
<Box mt={4}>
<CircularProgress />
</Box>
)}
{html && (
<>
<Box mt={4}>
<strong>Response</strong>
</Box>
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
<Typography fontFamily="monospace" fontSize="small">
{html}
</Typography>
</Paper>
</>
)}
</div>
);
};
@@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { contracts } from '@nymproject/contract-clients';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import { TableBody, TableCell, TableHead, TableRow } from '@mui/material';
import { settings } from './client';
const getClient = async () => {
const cosmWasmClient = await SigningCosmWasmClient.connect(settings.url);
const client = new contracts.Mixnet.MixnetQueryClient(cosmWasmClient, settings.mixnetContractAddress);
return client;
};
export const Mixnodes = () => {
const [mixnodes, setMixnodes] = useState<any>();
const [busy, setBusy] = useState<boolean>(false);
const getMixnodes = async () => {
setBusy(true);
const client = await getClient();
const { nodes } = await client.getMixNodesDetailed({});
setMixnodes(nodes);
setBusy(false);
};
if (busy) {
return (
<Box pt={4}>
<Stack direction="row" spacing={2} alignItems="center">
<CircularProgress />
<Typography>Loading...</Typography>
</Stack>
</Box>
);
}
if (!mixnodes) {
return (
<Box pt={4}>
<Button variant="outlined" onClick={getMixnodes}>
Query for mixnodes
</Button>
</Box>
);
}
return (
<Box pt={4}>
{mixnodes?.length && (
<Table size="small">
<TableHead>
<TableRow>
<TableCell>MixId</TableCell>
<TableCell>Owner Account</TableCell>
<TableCell>Layer</TableCell>
<TableCell>Bonded at Block Height</TableCell>
</TableRow>
</TableHead>
<TableBody>
{mixnodes.map((mixnode: any) => (
<TableRow key={mixnode.bond_information.mix_id}>
<TableCell>{mixnode.bond_information.mix_id}</TableCell>
<TableCell>{mixnode.bond_information.owner}</TableCell>
<TableCell>{mixnode.bond_information.layer}</TableCell>
<TableCell>{mixnode.bond_information.bonding_height}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Box>
);
};
+17
View File
@@ -0,0 +1,17 @@
import React, { FC } from 'react';
import { Chip, Link } from '@mui/material';
export const NPMLink: FC<{ packageName: string; kind: 'esm' | 'cjs'; preBundled?: boolean }> = ({
packageName,
kind,
preBundled,
}) => (
<Link
href={`https://www.npmjs.com/package/${packageName}`}
target="_blank"
sx={{ whiteSpace: 'nowrap', textDecoration: 'none' }}
>
{packageName} <Chip label={kind === 'cjs' ? 'CommonJS' : 'ESM'} size="small" />{' '}
{preBundled && <Chip label="pre-bundled" size="small" color="info" className="chipContained" />}
</Link>
);
+113
View File
@@ -0,0 +1,113 @@
import React, { useEffect, useState } from 'react';
import { createNymMixnetClient, NymMixnetClient, Payload } from '@nymproject/sdk-full-fat';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
const nymApiUrl = 'https://validator.nymtech.net/api';
export const Traffic = () => {
const [nym, setNym] = useState<NymMixnetClient>();
const [selfAddress, setSelfAddress] = useState<string>();
const [recipient, setRecipient] = useState<string>();
const [payload, setPayload] = useState<Payload>();
const [receivedMessage, setReceivedMessage] = useState<string>();
const [buttonEnabled, setButtonEnabled] = useState<boolean>(false);
const init = async () => {
const client = await createNymMixnetClient();
setNym(client);
// start the client and connect to a gateway
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
// check when is connected and set the self address
client?.events.subscribeToConnected((e) => {
const { address } = e.args;
setSelfAddress(address);
});
// show whether the client is ready or not
client?.events.subscribeToLoaded((e) => {
console.log('Client ready: ', e.args);
});
// show message payload content when received
client?.events.subscribeToTextMessageReceivedEvent((e) => {
console.log(e.args.payload);
setReceivedMessage(e.args.payload);
});
};
const stop = async () => {
await nym?.client.stop();
};
const send = () => payload && recipient && nym?.client.send({ payload, recipient });
useEffect(() => {
init();
return () => {
stop();
};
}, []);
useEffect(() => {
if (recipient && payload) {
setButtonEnabled(true);
} else {
setButtonEnabled(false);
}
}, [recipient, payload]);
if (!nym || !selfAddress) {
return (
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
);
}
return (
<Box padding={3}>
<Paper style={{ marginTop: '1rem', padding: '2rem' }}>
<Stack spacing={3}>
<Typography variant="body1">My self address is:</Typography>
<Typography variant="body1">{selfAddress || 'loading'}</Typography>
<Typography variant="h5">Communication through the Mixnet</Typography>
<TextField
type="text"
placeholder="Recipient Address"
onChange={(e) => setRecipient(e.target.value)}
size="small"
/>
<TextField
type="text"
placeholder="Message to send"
multiline
rows={4}
onChange={(e) => setPayload({ message: e.target.value, mimeType: 'text/plain' })}
size="small"
/>
<Button variant="outlined" onClick={() => send()} disabled={!buttonEnabled} sx={{ width: 'fit-content' }}>
Send
</Button>
</Stack>
{receivedMessage && (
<Stack spacing={3} style={{ marginTop: '1rem' }}>
<Typography variant="h5">Message Received!</Typography>
<Typography fontFamily="monospace">{receivedMessage}</Typography>
</Stack>
)}
</Paper>
</Box>
);
};
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import { useWalletContext } from './utils/wallet.context';
export const ConnectWallet = () => {
const { connect, balance, balanceLoading, accountLoading, account, clientsAreLoading } = useWalletContext();
const [mnemonic, setMnemonic] = useState<string>();
const [connectButtonText, setConnectButtonText] = useState<string>('Connect');
useEffect(() => {
if (accountLoading || clientsAreLoading || balanceLoading) {
setConnectButtonText('Loading...');
} else if (balance) {
setConnectButtonText('Connected');
}
setConnectButtonText('Connect');
}, [accountLoading, clientsAreLoading, balanceLoading]);
return (
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Typography variant="h5" textAlign="center">
Connect to your testnet account
</Typography>
<Box padding={3}>
<Typography variant="h6">Your testnet account:</Typography>
<Box marginY={3}>
<Typography variant="body1" marginBottom={3}>
Enter the mnemonic
</Typography>
<TextField
type="text"
placeholder="mnemonic"
onChange={(e) => setMnemonic(e.target.value)}
fullWidth
multiline
maxRows={4}
sx={{ marginBottom: 3 }}
/>
<Button
variant="outlined"
onClick={() => connect(mnemonic)}
disabled={!mnemonic || accountLoading || clientsAreLoading || balanceLoading}
>
{connectButtonText}
</Button>
</Box>
{account && balance ? (
<Box>
<Typography variant="body1">Address: {account}</Typography>
<Typography variant="body1">
Balance: {balance?.amount} {balance?.denom}
</Typography>
</Box>
) : (
<Box>
<Typography variant="body1">Please, enter your mnemonic to receive your account information</Typography>
</Box>
)}
</Box>
</Paper>
);
};
@@ -0,0 +1,129 @@
import React, { useEffect, useState } from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import Alert from '@mui/material/Alert';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { useWalletContext } from './utils/wallet.context';
export const Delegations = () => {
const { delegations, doDelegate, delegationLoader, unDelegateAll, unDelegateAllLoading, log } = useWalletContext();
const [delegationNodeId, setDelegationNodeId] = useState<string>();
const [amountToBeDelegated, setAmountToBeDelegated] = useState<string>();
const [infoText, setInfoText] = useState<string>('');
const cleanFields = () => {
setDelegationNodeId('');
setAmountToBeDelegated('');
setInfoText('');
};
useEffect(
() => () => {
cleanFields();
},
[],
);
return (
<Box>
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Box padding={3}>
<Typography variant="h6">Delegations</Typography>
<Box marginY={3}>
<Box marginY={3} display="flex" flexDirection="column">
<Typography marginBottom={3} variant="body1">
Make a delegation
</Typography>
<TextField
type="text"
placeholder="Mixnode ID"
onChange={(e) => setDelegationNodeId(e.target.value)}
size="small"
/>
<Box marginTop={3} display="flex" justifyContent="space-between">
<TextField
type="text"
placeholder="Amount"
onChange={(e) => setAmountToBeDelegated(e.target.value)}
size="small"
/>
<Button
variant="outlined"
onClick={() => {
doDelegate(delegationNodeId, amountToBeDelegated);
setInfoText('Changes will be visible after the next epoch');
cleanFields();
}}
disabled={delegationLoader}
>
{delegationLoader ? 'Delegation in process...' : 'Delegate'}
</Button>
</Box>
</Box>
</Box>
<Box marginTop={3}>
<Typography variant="body1">Your delegations:</Typography>
<Box marginBottom={3} display="flex" flexDirection="column">
{!delegations?.delegations?.length ? (
<Typography variant="body2">You do not have delegations</Typography>
) : (
<Box overflow="auto">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>MixId</TableCell>
<TableCell>Owner</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Cumulative Reward Ratio</TableCell>
</TableRow>
</TableHead>
<TableBody>
{delegations?.delegations.map((delegation: any) => (
<TableRow key={delegation.mix_id}>
<TableCell>{delegation.mix_id}</TableCell>
<TableCell>{delegation.owner}</TableCell>
<TableCell>{delegation.amount.amount}</TableCell>
<TableCell>{delegation.cumulative_reward_ratio}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
)}
</Box>
{delegations?.delegations.length > 0 && (
<Box marginBottom={3}>
<Button
variant="outlined"
onClick={() => {
unDelegateAll();
setInfoText('Changes will be visible after the next epoch');
}}
disabled={unDelegateAllLoading}
>
Undelegate All
</Button>
</Box>
)}
{infoText && <Alert severity="info">{infoText}</Alert>}
</Box>
</Box>
</Paper>
{log?.node?.length > 0 && log.type === 'delegate' && (
<Box marginTop={3}>
<Typography variant="h5">Transaction Logs:</Typography>
{log.node}
</Box>
)}
</Box>
);
};
@@ -0,0 +1,69 @@
import React, { useState, useEffect } from 'react';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import { useWalletContext } from './utils/wallet.context';
export const SendTokes = () => {
const { sendingTokensLoading, sendTokens, log } = useWalletContext();
const [recipientAddress, setRecipientAddress] = useState<string>();
const [tokensToSend, setTokensToSend] = useState<string>();
const cleanFields = () => {
setRecipientAddress('');
setTokensToSend('');
};
useEffect(
() => () => {
cleanFields();
},
[],
);
return (
<Box>
<Paper style={{ marginTop: '1rem', padding: '1rem' }}>
<Box padding={3}>
<Typography variant="h6">Send Tokens</Typography>
<Box marginTop={3} display="flex" flexDirection="column">
<TextField
type="text"
placeholder="Recipient Address"
onChange={(e) => setRecipientAddress(e.target.value)}
size="small"
/>
<Box marginY={3} display="flex" justifyContent="space-between">
<TextField
type="text"
placeholder="Amount"
onChange={(e) => setTokensToSend(e.target.value)}
size="small"
/>
<Button
variant="outlined"
onClick={() => {
sendTokens(recipientAddress, tokensToSend);
cleanFields();
}}
disabled={sendingTokensLoading}
>
{sendingTokensLoading ? 'Sending...' : 'Send tokens'}
</Button>
</Box>
</Box>
</Box>
</Paper>
{log?.node?.length > 0 && log.type === 'sendTokens' && (
<Box marginTop={3}>
<Typography variant="h5">Transaction Logs:</Typography>
{log.node}
</Box>
)}
</Box>
);
};
@@ -0,0 +1,262 @@
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
import { Coin } from '@cosmjs/stargate';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { settings } from '../../client';
import { signerAccount, fetchSignerCosmosWasmClient, fetchSignerClient } from './wallet.methods';
/**
* This context provides the state for wallet.
*/
interface WalletState {
accountLoading: boolean;
account: string;
clientsAreLoading: boolean;
connect?: (mnemonic: string) => void;
balance?: Coin;
balanceLoading: boolean;
setRecipientAddress?: (value: string) => void;
setTokensToSend?: (value: string) => void;
sendingTokensLoading: boolean;
log?: { type: 'delegate' | 'sendTokens'; node: React.ReactNode[] };
sendTokens?: (recipientAddress: string, tokensToSend: string) => void;
delegations?: any;
doDelegate?: (mixId: string, amount: string) => void;
delegationLoader?: boolean;
unDelegateAll?: () => void;
unDelegateAllLoading?: boolean;
}
export const WalletContext = createContext<WalletState>({
accountLoading: false,
account: '',
clientsAreLoading: false,
balanceLoading: false,
sendingTokensLoading: false,
});
export const useWalletContext = (): React.ContextType<typeof WalletContext> => useContext<WalletState>(WalletContext);
export const WalletContextProvider = ({ children }: { children: JSX.Element }) => {
const [cosmWasmSignerClient, setCosmWasmSignerClient] = useState<SigningCosmWasmClient>(null);
const [nymWasmSignerClient, setNymWasmSignerClient] = useState<any>(null);
const [account, setAccount] = useState<string>('');
const [accountLoading, setAccountLoading] = useState<boolean>(false);
const [delegations, setDelegations] = useState<{ delegations: any[]; start_next_after: any }>();
const [clientsAreLoading, setClientsAreLoading] = useState<boolean>(false);
const [balance, setBalance] = useState<Coin>(null);
const [balanceLoading, setBalanceLoading] = useState<boolean>(false);
const [sendingTokensLoading, setSendingTokensLoading] = useState<boolean>(false);
const [log, setLog] = useState<{ type: 'delegate' | 'sendTokens'; node: React.ReactNode[] }>();
const [delegationLoader, setDelegationLoader] = useState<boolean>(false);
const [unDelegateAllLoading, setUnDelegateAllLoading] = useState<boolean>(false);
const Reset = () => {
setAccountLoading(false);
setDelegations(null);
setClientsAreLoading(false);
setBalance(null);
setBalanceLoading(false);
setSendingTokensLoading(false);
};
const getSignerAccount = async (mnemonic: string) => {
setAccountLoading(true);
try {
const signer = await signerAccount(mnemonic);
const accounts = await signer.getAccounts();
if (accounts[0]) {
setAccount(accounts[0].address);
}
} catch (error) {
console.error(error);
}
setAccountLoading(false);
};
const getClients = async (mnemonic: string) => {
setClientsAreLoading(true);
try {
setCosmWasmSignerClient(await fetchSignerCosmosWasmClient(mnemonic));
setNymWasmSignerClient(await fetchSignerClient(mnemonic));
} catch (error) {
console.error(error);
}
setClientsAreLoading(false);
};
const connect = async (mnemonic: string) => {
getSignerAccount(mnemonic);
getClients(mnemonic);
};
const getBalance = useCallback(async () => {
setBalanceLoading(true);
try {
const newBalance = await cosmWasmSignerClient?.getBalance(account, 'unym');
setBalance(newBalance);
} catch (error) {
console.error(error);
}
setBalanceLoading(false);
}, [account, cosmWasmSignerClient]);
const getDelegations = useCallback(async () => {
const delegationsReceived = await nymWasmSignerClient.getDelegatorDelegations({
delegator: settings.address,
});
setDelegations(delegationsReceived);
}, [nymWasmSignerClient]);
const sendTokens = async (recipientAddress: string, tokensToSend: string) => {
const memo: string = 'test sending tokens';
setSendingTokensLoading(true);
try {
const res = await cosmWasmSignerClient.sendTokens(
account,
recipientAddress,
[{ amount: tokensToSend, denom: 'unym' }],
'auto',
memo,
);
setLog({
type: 'sendTokens',
node: [
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
],
});
} catch (error) {
console.error(error);
}
setSendingTokensLoading(false);
};
const doDelegate = async (mixId: string, amount: string) => {
setDelegationLoader(true);
const memo: string = 'test delegation';
const coinAmount: Coin = { amount, denom: 'unym' };
try {
const res = await nymWasmSignerClient.delegateToMixnode({ mixId: parseInt(mixId, 10) }, 'auto', memo, [
coinAmount,
]);
setLog({
type: 'delegate',
node: [
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
],
});
} catch (error) {
console.error(error);
}
setDelegationLoader(false);
};
const unDelegateAll = async () => {
setUnDelegateAllLoading(true);
try {
const logs: React.ReactNode[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const delegation of delegations.delegations) {
// eslint-disable-next-line no-await-in-loop
const res = await nymWasmSignerClient.undelegateFromMixnode({ mixId: delegation.mix_id }, 'auto');
setUnDelegateAllLoading(false);
logs.push(
<div key={JSON.stringify(res, null, 2)}>
<code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
<pre>{JSON.stringify(res, null, 2)}</pre>
</div>,
);
}
setLog({
type: 'delegate',
node: logs,
});
} catch (error) {
console.error(error);
setUnDelegateAllLoading(false);
}
};
// const withdrawRewards = async () => {
// const validatorAdress = '';
// const memo = 'test withdraw rewards';
// setWithdrawLoading(true);
// try {
// const res = await cosmWasmSignerClient.withdrawRewards(account, validatorAdress, 'auto', memo);
// setLog({
// type: 'delegate',
// node: [
// <div key={JSON.stringify(res, null, 2)}>
// <code style={{ marginRight: '2rem' }}>{new Date().toLocaleTimeString()}</code>
// <pre>{JSON.stringify(res, null, 2)}</pre>
// </div>,
// ],
// });
// } catch (error) {
// console.error(error);
// }
// setWithdrawLoading(false);
// };
useEffect(
() => () => {
Reset();
},
[],
);
useEffect(() => {
if (cosmWasmSignerClient) {
getBalance();
}
}, [cosmWasmSignerClient]);
useEffect(() => {
if (nymWasmSignerClient) {
getDelegations();
}
}, [nymWasmSignerClient]);
const state = useMemo<WalletState>(
() => ({
accountLoading,
account,
clientsAreLoading,
connect,
balance,
balanceLoading,
sendingTokensLoading,
log,
sendTokens,
delegations,
doDelegate,
delegationLoader,
unDelegateAll,
unDelegateAllLoading,
}),
[
accountLoading,
account,
clientsAreLoading,
connect,
balance,
balanceLoading,
sendingTokensLoading,
log,
sendTokens,
delegations,
doDelegate,
delegationLoader,
unDelegateAll,
unDelegateAllLoading,
],
);
return <WalletContext.Provider value={state}>{children}</WalletContext.Provider>;
};
@@ -0,0 +1,50 @@
import { contracts } from '@nymproject/contract-clients';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { GasPrice } from '@cosmjs/stargate';
import { settings } from '../../client';
export const signerAccount = async (mnemonic: string) => {
// create a wallet to sign transactions with the mnemonic
const signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
prefix: 'n',
});
return signer;
};
export const fetchSignerCosmosWasmClient = async (mnemonic: string) => {
const signer = await signerAccount(mnemonic);
// create a signing client we don't need to set the gas price conversion for queries
const cosmWasmClient = await SigningCosmWasmClient.connectWithSigner(settings.url, signer, {
gasPrice: GasPrice.fromString('0.025unym'),
});
return cosmWasmClient;
};
export const fetchSignerClient = async (mnemonic: string) => {
const signer = await signerAccount(mnemonic);
// create a signing client we don't need to set the gas price conversion for queries
// if you want to connect without signer you'd write ".connect" and "url" as param
const cosmWasmClient = await SigningCosmWasmClient.connectWithSigner(settings.url, signer, {
gasPrice: GasPrice.fromString('0.025unym'),
});
/** create a mixnet contract client
* @param cosmWasmClient the client to use for signing and querying
* @param settings.address the bech32 address prefix (human readable part)
* @param settings.mixnetContractAddress the bech32 address prefix (human readable part)
* @returns the client in MixnetClient form
*/
const mixnetClient = new contracts.Mixnet.MixnetClient(
cosmWasmClient,
settings.address, // sender (that account of the signer)
settings.mixnetContractAddress, // contract address (different on mainnet, QA, etc)
);
return mixnetClient;
};

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+50
View File
@@ -0,0 +1,50 @@
// const path = require('path');
// const CopyPlugin = require('copy-webpack-plugin');
const withNextra = require('nextra')({
theme: 'nextra-theme-docs',
themeConfig: './theme.config.tsx',
});
const nextra = withNextra();
nextra.webpack = (config, options) => {
// generate Nextra's webpack config
const newConfig = withNextra().webpack(config, options);
newConfig.module.rules.push({
test: /\.txt$/i,
use: 'raw-loader',
});
// TODO: figure out how to properly bundle WASM and workers with Nextra
// newConfig.plugins.push(
// new CopyPlugin({
// patterns: [
// {
// from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), '*.wasm'),
// to: '[name][ext]',
// context: path.resolve(__dirname, 'out'),
// },
// {
// from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), '*worker*.js'),
// to: '[name][ext]',
// context: path.resolve(__dirname, 'out'),
// },
// ],
// }),
// );
return newConfig;
};
const config = {
...nextra,
// output: 'export', // static HTML files, has problems with Vercel
// rewrites: undefined,
images: {
unoptimized: true,
},
transpilePackages: ['@nymproject/contract-clients'],
};
module.exports = config;
+54
View File
@@ -0,0 +1,54 @@
{
"name": "@nymproject/docs",
"version": "1.0.0",
"description": "Nym Docs Monorepo",
"license": "Apache-2.0",
"author": "Nym Technologies SA",
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"lint:fix": "next lint --fix",
"start": "next start"
},
"dependencies": {
"@cosmjs/amino": "^0.32.2",
"@cosmjs/cosmwasm-launchpad": "^0.25.6",
"@cosmjs/cosmwasm-stargate": "^0.32.2",
"@cosmjs/encoding": "^0.32.2",
"@cosmjs/proto-signing": "^0.32.2",
"@cosmjs/stargate": "^0.32.2",
"@cosmos-kit/core": "^2.8.9",
"@cosmos-kit/keplr": "^2.6.9",
"@cosmos-kit/keplr-extension": "^2.7.9",
"@cosmos-kit/ledger": "^2.6.9",
"@cosmos-kit/react": "^2.10.11",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@interchain-ui/react": "^1.8.0",
"@mui/icons-material": "^5.14.9",
"@mui/lab": "^5.0.0-alpha.145",
"@mui/material": "^5.14.8",
"@nymproject/contract-clients": ">=1.2.4-rc.2 || ^1",
"@nymproject/mix-fetch-full-fat": ">=1.2.4-rc.2 || ^1",
"@nymproject/sdk-full-fat": ">=1.2.4-rc.2 || ^1",
"chain-registry": "^1.19.0",
"cosmjs-types": "^0.9.0",
"next": "^13.4.19",
"nextra": "latest",
"nextra-theme-docs": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"save": "^2.9.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "18.11.10",
"copy-webpack-plugin": "^11.0.0",
"eslint": "8.46.0",
"eslint-config-next": "13.4.13",
"raw-loader": "^4.0.2",
"typescript": "^4.9.3"
},
"private": false
}
+27
View File
@@ -0,0 +1,27 @@
import React, { useMemo } from 'react';
import type { AppProps } from 'next/app';
import './styles.css';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => {
const muiTheme = useMemo(
() =>
createTheme({
palette: {
mode: 'dark',
primary: {
main: '#e67300',
},
},
}),
[],
);
const AnyComponent = Component as any;
return (
<ThemeProvider theme={muiTheme}>
<AnyComponent {...pageProps} />
</ThemeProvider>
);
};
export default MyApp;
+28
View File
@@ -0,0 +1,28 @@
{
"network": {
"title": "Network",
"type": "page"
},
"developers": {
"title": "Developers",
"type": "page"
},
"sdk": {
"title": "SDKs",
"type": "menu",
"items": {
"rust": {
"title": "Rust SDK",
"href": "/sdk/rust"
},
"typescript": {
"title": "Typescript SDK",
"href": "/sdk/typescript"
}
}
},
"operators": {
"title": "Operators",
"type": "page"
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"network": {
"title": "Network",
"type": "page"
},
"developers": {
"title": "Developers",
"type": "menu",
"items": {
"concepts": {
"title": "Core Concepts",
"href": "/developers/concepts"
},
"integrations": {
"title": "Integrations",
"href": "/developers/integrations"
},
"tools": {
"title": "Tools",
"href": "/developers/tools"
}
}
},
"sdk": {
"title": "SDKs",
"type": "menu",
"items": {
"rust": {
"title": "Rust SDK",
"href": "/sdk/rust"
},
"typescript": {
"title": "Typescript SDK",
"href": "/sdk/typescript"
}
}
},
"operators": {
"title": "Operators",
"type": "page"
}
}
@@ -0,0 +1,9 @@
{
"concepts": "Core Concepts",
"integrations": "Integrations",
"tools": "Tools",
"---": {
"type": "separator"
},
"licensing": "Licensing"
}
@@ -0,0 +1 @@
stub
@@ -0,0 +1,5 @@
{
"overview": "Overview",
"messages": "Message Based Paradigm",
"abstractions": "Connection Abstractions"
}
@@ -0,0 +1 @@
stub
@@ -0,0 +1 @@
stub
@@ -0,0 +1 @@
stub
@@ -0,0 +1,3 @@
# Tools
There are a few tools available to developers for chain interaction: the `nym-cli` tool, which operates as an easier-to-use wrapper around `nyxd`, and the CLI wallet.
@@ -0,0 +1,4 @@
{
"nym-cli": "NYM-CLI tool",
"cli-wallet": "CLI wallet"
}
+1
View File
@@ -0,0 +1 @@
This should be the landing page with w/ever nav we want
@@ -0,0 +1,12 @@
{
"index": "Introduction",
"concepts": "Core Concepts",
"architecture": "Architecture",
"traffic": "Traffic Flow",
"cryptography": "Crypto",
"rewards": "Rewards",
"---": {
"type": "separator"
},
"licensing": "Licencing"
}

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 20 MiB

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Some files were not shown because too many files have changed in this diff Show More