Skip to content

Commit 607a57c

Browse files
author
BuiVanQuangHung
committed
feat: display transfer offers
1 parent a726258 commit 607a57c

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import {
4+
AmountDisplay,
5+
DateDisplay,
6+
ErrorDisplay,
7+
Loading,
8+
} from '@lfdecentralizedtrust/splice-common-frontend';
9+
import BigNumber from 'bignumber.js';
10+
import * as React from 'react';
11+
import { useCallback, useMemo, useState } from 'react';
12+
13+
import { ArrowCircleLeftOutlined } from '@mui/icons-material';
14+
import { Box, Button, Card, CardContent, Chip, Stack } from '@mui/material';
15+
import Typography from '@mui/material/Typography';
16+
17+
import { AmuletTransferInstruction } from '@daml.js/splice-amulet-0.1.9/lib/Splice/AmuletTransferInstruction';
18+
import { Unit } from '@daml.js/splice-wallet-payments/lib/Splice/Wallet/Payment';
19+
import { TransferOffer } from '@daml.js/splice-wallet/lib/Splice/Wallet/TransferOffer/module';
20+
import { ContractId } from '@daml/types';
21+
22+
import { usePrimaryParty, useTransferOffers } from '../hooks';
23+
import useAmuletPrice from '../hooks/scan-proxy/useAmuletPrice';
24+
import { useTokenStandardTransfers } from '../hooks/useTokenStandardTransfers';
25+
import { WalletTransferOffer } from '../models/models';
26+
import { useWalletConfig } from '../utils/config';
27+
import { convertCurrency } from '../utils/currencyConversion';
28+
import BftAnsEntry from './BftAnsEntry';
29+
30+
type PartialWalletTransferOffer = {
31+
contractId: ContractId<TransferOffer> | ContractId<AmuletTransferInstruction>;
32+
amount: string;
33+
sender: string;
34+
expiresAt: string;
35+
isTokenStandard: boolean;
36+
};
37+
export const PendingOffers: React.FC = () => {
38+
const [offers, setOffers] = useState<WalletTransferOffer[]>([]);
39+
const amuletPriceQuery = useAmuletPrice();
40+
const primaryPartyId = usePrimaryParty();
41+
42+
const toWalletTransferOffer = useCallback(
43+
async (
44+
items: Array<PartialWalletTransferOffer>,
45+
amuletPrice: BigNumber
46+
): Promise<WalletTransferOffer[]> => {
47+
return items
48+
.filter(item => item.sender === primaryPartyId)
49+
.map(item => {
50+
return {
51+
contractId: item.contractId,
52+
ccAmount: item.amount,
53+
usdAmount: amuletPrice ? amuletPrice.times(item.amount).toString() : '...',
54+
conversionRate: amuletPrice ? amuletPrice?.toString() : '...',
55+
convertedCurrency: convertCurrency(
56+
BigNumber(item.amount),
57+
Unit.AmuletUnit,
58+
amuletPrice
59+
),
60+
senderId: item.sender,
61+
expiry: item.expiresAt,
62+
isTokenStandard: item.isTokenStandard,
63+
};
64+
});
65+
},
66+
[primaryPartyId]
67+
);
68+
69+
const transferOfferContractsQuery = useTransferOffers(amuletPriceQuery.data);
70+
const { data: transferOfferContracts } = transferOfferContractsQuery;
71+
const tokenStandardTransfersQuery = useTokenStandardTransfers();
72+
const { data: tokenStandardTransferContracts } = tokenStandardTransfersQuery;
73+
const amuletPrice = amuletPriceQuery.data;
74+
75+
useMemo(() => {
76+
if (transferOfferContracts && tokenStandardTransferContracts && amuletPrice) {
77+
const allTransfers: PartialWalletTransferOffer[] = transferOfferContracts
78+
.map(offer => {
79+
const item: PartialWalletTransferOffer = {
80+
isTokenStandard: false,
81+
contractId: offer.contractId,
82+
amount: offer.payload.amount.amount,
83+
sender: offer.payload.sender,
84+
expiresAt: offer.payload.expiresAt,
85+
};
86+
return item;
87+
})
88+
.concat(
89+
tokenStandardTransferContracts.map(transfer => {
90+
const item: PartialWalletTransferOffer = {
91+
isTokenStandard: true,
92+
contractId: transfer.contractId,
93+
amount: transfer.payload.transfer.amount,
94+
sender: transfer.payload.transfer.sender,
95+
expiresAt: transfer.payload.transfer.executeBefore,
96+
};
97+
return item;
98+
})
99+
);
100+
toWalletTransferOffer(allTransfers, amuletPrice).then(setOffers);
101+
}
102+
}, [amuletPrice, toWalletTransferOffer, transferOfferContracts, tokenStandardTransferContracts]);
103+
104+
const isLoading =
105+
amuletPriceQuery.isLoading ||
106+
transferOfferContractsQuery.isLoading ||
107+
tokenStandardTransfersQuery.isLoading;
108+
const isError =
109+
amuletPriceQuery.isError ||
110+
transferOfferContractsQuery.isError ||
111+
tokenStandardTransfersQuery.isError;
112+
113+
return (
114+
<Stack spacing={4} direction="column" justifyContent="center" id="transfer-offers">
115+
<Typography mt={6} variant="h4">
116+
Action Needed{' '}
117+
<Chip label={offers.length} color="success" className="transfer-offers-count" />
118+
</Typography>
119+
{isLoading ? (
120+
<Loading />
121+
) : isError ? (
122+
<ErrorDisplay message={'Error while fetching amulet price and transfer offers'} />
123+
) : offers.length === 0 ? (
124+
<Box display="flex" justifyContent="center">
125+
<Typography variant="h6">No transfer offers available</Typography>
126+
</Box>
127+
) : (
128+
offers.map((offer, index) => (
129+
<TransferOfferDisplay key={'offer-' + index} transferOffer={offer} />
130+
))
131+
)}
132+
</Stack>
133+
);
134+
};
135+
136+
interface TransferOfferProps {
137+
transferOffer: WalletTransferOffer;
138+
}
139+
140+
export const TransferOfferDisplay: React.FC<TransferOfferProps> = props => {
141+
const config = useWalletConfig();
142+
const offer = props.transferOffer;
143+
144+
return (
145+
<Card className="transfer-offer" variant="outlined">
146+
<CardContent
147+
sx={{
148+
display: 'flex',
149+
direction: 'row',
150+
justifyContent: 'space-between',
151+
alignItems: 'center',
152+
}}
153+
>
154+
<ArrowCircleLeftOutlined fontSize="large" />
155+
<Stack direction="row" alignItems="center">
156+
<Stack direction="column">
157+
<BftAnsEntry
158+
partyId={offer.senderId}
159+
variant="h5"
160+
className={'transfer-offer-sender'}
161+
/>
162+
</Stack>
163+
</Stack>
164+
<Stack direction="column" alignItems="flex-end">
165+
<Typography className="transfer-offer-amulet-amount">
166+
+ <AmountDisplay amount={BigNumber(offer.ccAmount)} currency="AmuletUnit" />
167+
</Typography>
168+
<Typography className="transfer-offer-usd-amount-rate">
169+
<>
170+
<AmountDisplay
171+
amount={offer.convertedCurrency.amount}
172+
currency={offer.convertedCurrency.currency}
173+
/>{' '}
174+
@ {offer.convertedCurrency.amuletPriceToShow.toString()}{' '}
175+
{config.spliceInstanceNames.amuletNameAcronym}/USD
176+
</>
177+
</Typography>
178+
</Stack>
179+
180+
<Stack direction="row" alignItems="center" spacing={2}>
181+
<Button variant="outlined" size="small" className="transfer-offer-accept">
182+
Pending
183+
</Button>
184+
</Stack>
185+
186+
<Typography variant="caption" className="transfer-offer-expiry">
187+
Expires <DateDisplay datetime={offer.expiry} />
188+
</Typography>
189+
</CardContent>
190+
</Card>
191+
);
192+
};

apps/wallet/frontend/src/components/SendTransfer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import useLookupTransferPreapproval from '../hooks/scan-proxy/useLookupTransferP
3232
import BftAnsField from './BftAnsField';
3333
import { useFeatureSupport } from '../hooks/useFeatureSupport';
3434
import AmountInput from './AmountInput';
35+
import { PendingOffers } from './PendingOffers';
3536

3637
const SendTransfer: React.FC = () => {
3738
const { createTransferOffer, transferPreapprovalSend, createTransferViaTokenStandard } =
@@ -252,6 +253,7 @@ const SendTransfer: React.FC = () => {
252253
</DisableConditionally>
253254
</CardContent>
254255
</Card>
256+
<PendingOffers />
255257
</Stack>
256258
);
257259
};

0 commit comments

Comments
 (0)