Skip to content

Commit

Permalink
Wallet nft transfer1 (MystenLabs#2848)
Browse files Browse the repository at this point in the history
* nft transfer

* wallet transfer
  • Loading branch information
Jibz1 authored Jun 30, 2022
1 parent c7db29f commit 1fce162
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 14 deletions.
7 changes: 7 additions & 0 deletions wallet/src/ui/app/components/sui-object/SuiObject.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,10 @@
color: #43b0e6;
text-decoration: none;
}

:global(.btn).send {
font-size: 11px;
font-weight: 300;
padding: 4px;
margin-left: 5px;
}
39 changes: 30 additions & 9 deletions wallet/src/ui/app/components/sui-object/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

import { isSuiMoveObject, isSuiMovePackage } from '@mysten/sui.js';
import { memo } from 'react';
import cl from 'classnames';
import { memo, useMemo } from 'react';
import { Link } from 'react-router-dom';

import Field from './field';
import CopyToClipboard from '_components/copy-to-clipboard';
Expand All @@ -16,9 +18,10 @@ import st from './SuiObject.module.scss';

export type SuiObjectProps = {
obj: SuiObjectType;
sendNFT?: boolean;
};

function SuiObject({ obj }: SuiObjectProps) {
function SuiObject({ obj, sendNFT }: SuiObjectProps) {
const { objectId } = obj.reference;
const shortId = useMiddleEllipsis(objectId);
const objType =
Expand All @@ -28,6 +31,11 @@ function SuiObject({ obj }: SuiObjectProps) {
const suiMoveObjectFields = isSuiMoveObject(obj.data)
? obj.data.fields
: null;

const sendUrl = useMemo(
() => `/send-nft?${new URLSearchParams({ objectId }).toString()}`,
[objectId]
);
return (
<div className={st.container}>
<span className={st.id} title={objectId}>
Expand All @@ -44,13 +52,26 @@ function SuiObject({ obj }: SuiObjectProps) {
</>
) : null}
<div className={st.fields}>
{suiMoveObjectFields
? keys.map((aField) => (
<Field key={aField} name={aField}>
{String(suiMoveObjectFields[aField])}
</Field>
))
: null}
{suiMoveObjectFields ? (
<>
{keys.map((aField) => (
<Field key={aField} name={aField}>
{String(suiMoveObjectFields[aField])}
</Field>
))}
{sendNFT ? (
<div>
<Link
className={cl('btn', st.send)}
to={sendUrl}
>
Send NFT
</Link>
</div>
) : null}
</>
) : null}

{isSuiMovePackage(obj.data) ? (
<Field name="disassembled">
{JSON.stringify(obj.data.disassembled).substring(
Expand Down
2 changes: 2 additions & 0 deletions wallet/src/ui/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import SelectPage from './pages/initialize/select';
import SiteConnectPage from './pages/site-connect';
import TransactionDetailsPage from './pages/transaction-details';
import TransferCoinPage from './pages/transfer-coin';
import TransferNFTPage from './pages/transfer-nft';
import WelcomePage from './pages/welcome';
import { AppType } from './redux/slices/app/AppType';
import { useAppDispatch, useAppSelector } from '_hooks';
Expand Down Expand Up @@ -45,6 +46,7 @@ const App = () => {
<Route path="transactions" element={<TransactionsPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="send" element={<TransferCoinPage />} />
<Route path="send-nft" element={<TransferNFTPage />} />
<Route
path="tx/:txDigest"
element={<TransactionDetailsPage />}
Expand Down
6 changes: 5 additions & 1 deletion wallet/src/ui/app/pages/home/nfts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ function NftsPage() {
return (
<ObjectsLayout totalItems={nfts.length} emptyMsg="No NFTs found">
{nfts.map((anNft) => (
<SuiObject obj={anNft} key={anNft.reference.objectId} />
<SuiObject
obj={anNft}
sendNFT={true}
key={anNft.reference.objectId}
/>
))}
</ObjectsLayout>
);
Expand Down
39 changes: 39 additions & 0 deletions wallet/src/ui/app/pages/transfer-nft/TransferNFTForm.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.container {
display: flex;
flex-flow: column nowrap;
flex: 1;
align-self: stretch;
}

.group {
display: flex;
flex-flow: column nowrap;

& + & {
margin-top: 8px;
}
}

.label {
margin-bottom: 4px;
font-weight: 600;
}

.error {
color: #8b1111;
line-height: 1em;
min-height: 1em;
margin-top: 2px;
margin-bottom: 5px;
}

.muted {
font-size: 12px;
font-weight: 400;
color: #666;
margin-top: 1px;
}

.input {
padding: 12px;
}
104 changes: 104 additions & 0 deletions wallet/src/ui/app/pages/transfer-nft/TransferNFTForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { ErrorMessage, Field, Form, useFormikContext } from 'formik';
import { useEffect, useRef, memo } from 'react';
import { useIntl } from 'react-intl';

import AddressInput from '_components/address-input';
import Alert from '_components/alert';
import LoadingIndicator from '_components/loading/LoadingIndicator';
import NumberInput from '_components/number-input';
import { balanceFormatOptions } from '_shared/formatting';

import type { FormValues } from '.';

import st from './TransferNFTForm.module.scss';

export type TransferNFTFormProps = {
submitError: string | null;
gasBalance: string;
onClearSubmitError: () => void;
};

function TransferNFTForm({
submitError,
gasBalance,
onClearSubmitError,
}: TransferNFTFormProps) {
const {
isSubmitting,
isValid,
values: { to, amount },
} = useFormikContext<FormValues>();
const intl = useIntl();
const onClearRef = useRef(onClearSubmitError);
onClearRef.current = onClearSubmitError;
const transferCost = 10000;
useEffect(() => {
onClearRef.current();
}, [to, amount]);
return (
<Form className={st.container} autoComplete="off" noValidate={true}>
<div className={st.group}>
<label className={st.label}>To:</label>
<Field
component={AddressInput}
name="to"
className={st.input}
/>
<div className={st.muted}>The recipient&apos;s address</div>
<ErrorMessage className={st.error} name="to" component="div" />
</div>

<div className={st.group}>
<label className={st.label}>Amount:</label>
<Field
component={NumberInput}
allowNegative={false}
name="amount"
value={transferCost}
className={st.input}
disabled={true}
/>
<div className={st.muted}>
Available balance:{' '}
{intl.formatNumber(
BigInt(gasBalance),
balanceFormatOptions
)}{' '}
</div>
<ErrorMessage
className={st.error}
name="amount"
component="div"
/>
</div>

{BigInt(gasBalance) < transferCost && (
<div className={st.error}>
* Insufficient balance to cover transfer cost
</div>
)}
{submitError ? (
<div className={st.group}>
<Alert>
<strong>Transfer failed.</strong>{' '}
<small>{submitError}</small>
</Alert>
</div>
) : null}
<div className={st.group}>
<button
type="submit"
disabled={!isValid || isSubmitting}
className="btn"
>
{isSubmitting ? <LoadingIndicator /> : 'Send'}
</button>
</div>
</Form>
);
}

export default memo(TransferNFTForm);
126 changes: 126 additions & 0 deletions wallet/src/ui/app/pages/transfer-nft/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Formik } from 'formik';
import { useCallback, useMemo, useState } from 'react';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';

import TransferNFTForm from './TransferNFTForm';
import { createValidationSchema } from './validation';
import Loading from '_components/loading';
import SuiObject from '_components/sui-object';
import { useAppSelector, useAppDispatch } from '_hooks';
import {
accountAggregateBalancesSelector,
accountNftsSelector,
} from '_redux/slices/account';
import { transferSuiNFT } from '_redux/slices/sui-objects';
import { GAS_TYPE_ARG } from '_redux/slices/sui-objects/Coin';

import type { SerializedError } from '@reduxjs/toolkit';
import type { FormikHelpers } from 'formik';

const initialValues = {
to: '',
amount: 10000,
};

export type FormValues = typeof initialValues;

function TransferNFTPage() {
const [searchParams] = useSearchParams();
const objectId = useMemo(
() => searchParams.get('objectId'),
[searchParams]
);
const address = useAppSelector(
({ account: { address } }) => address && `0x${address}`
);

let selectedNFT;
const nftCollections = useAppSelector(accountNftsSelector);
if (nftCollections && nftCollections.length) {
selectedNFT = nftCollections.filter(
(nftItems) => nftItems.reference.objectId === objectId
)[0];
}

const aggregateBalances = useAppSelector(accountAggregateBalancesSelector);

const gasAggregateBalance = useMemo(
() => aggregateBalances[GAS_TYPE_ARG] || BigInt(0),
[aggregateBalances]
);

const [sendError, setSendError] = useState<string | null>(null);

const validationSchema = useMemo(
() =>
createValidationSchema(
gasAggregateBalance,
address || '',
objectId || ''
),
[gasAggregateBalance, address, objectId]
);
const dispatch = useAppDispatch();
const navigate = useNavigate();
const onHandleSubmit = useCallback(
async (
{ to }: FormValues,
{ resetForm }: FormikHelpers<FormValues>
) => {
if (objectId === null) {
return;
}
setSendError(null);
try {
await dispatch(
transferSuiNFT({
recipientAddress: to,
nftId: objectId,
})
).unwrap();
resetForm();
navigate('/nfts/');
} catch (e) {
setSendError((e as SerializedError).message || null);
}
},
[dispatch, navigate, objectId]
);
const handleOnClearSubmitError = useCallback(() => {
setSendError(null);
}, []);
const loadingBalance = useAppSelector(
({ suiObjects }) => suiObjects.loading && !suiObjects.lastSync
);

if (!objectId || !selectedNFT) {
return <Navigate to="/nfts" replace={true} />;
}

return (
<>
<h3>Send This NFT</h3>
<SuiObject obj={selectedNFT} />
<br />
<Loading loading={loadingBalance}>
<Formik
initialValues={initialValues}
validateOnMount={true}
validationSchema={validationSchema}
onSubmit={onHandleSubmit}
>
<TransferNFTForm
submitError={sendError}
gasBalance={gasAggregateBalance.toString()}
onClearSubmitError={handleOnClearSubmitError}
/>
</Formik>
</Loading>
</>
);
}

export default TransferNFTPage;
Loading

0 comments on commit 1fce162

Please sign in to comment.