Skip to content

Commit a13be3a

Browse files
marcomariscalgaryghayratjferas
authored
feat: save user announcements to local storage and last fetched block (#685)
* feat: set the last fetched block as the start block * feat: handle caching user announcements and latest fetched block * feat: show user announcements if there are any * fix: handle watching/loading announcements * fix: parse out lastFetchedBlock and fix user announcement loading logic * chore: log * feat: handle block data caching * feat: show most recent block data if exists * fix: type check * feat: handle user announcements already present and sign language * feat: only show fetching when no user announcements * feat: fetching latest from last fetched block component * feat: fetching latest translation for cn * feat: clear local storage button and functionality * fix: start block handling logic * feat: dedupe user announcements * fix: logic * fix: minimize debugging logs on userAnnouncement changes * feat: handle scanning latest announcements from last fetched block * feat: sort by timestamp explicitly * feat: no loading sequence when there are announcements * fix: need sig lately verbiage * fix: add need sig lately to cn * fix: little more mb * fix: no withdraw verbiage on need-sig-lately * feat: handle need sig * Update frontend/src/i18n/locales/en-US.json Co-authored-by: Gary Ghayrat <[email protected]> * feat: handle sign button instead of needs sig * Update frontend/src/i18n/locales/zh-CN.json Co-authored-by: Gary Ghayrat <[email protected]> * fix: move local storage clear button above lang * fix: spacing more uniform * fix: use computed ref as param, and set setIsInWithdrawFlow to false on mount * feat: sign and withdraw * fix: contract periphery tests (#688) * fix: explicitly sort the tokens by addr * fix: use vm.computeCreateAddress * fix: mirror test sender params * fix: use actual owner * fix: add back gnosis * Remove all reference to INFURA_ID (#687) --------- Co-authored-by: John Feras <[email protected]> * fix: use balanceIndex to ensure that the correct balance is fetched from the stealthBalances array * fix: dedupe by tx hash and receiver instead of just tx hash * fix: include receiver to derive isWithdrawn * fix: img --------- Co-authored-by: Gary Ghayrat <[email protected]> Co-authored-by: John Feras <[email protected]>
1 parent 8e9f414 commit a13be3a

21 files changed

+490
-104
lines changed

.github/workflows/ci.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ env:
2828
GNOSIS_CHAIN_RPC_URL: ${{ secrets.GNOSIS_CHAIN_RPC_URL }}
2929
BASE_RPC_URL: $${{ secrets.BASE_RPC_URL }}
3030
FOUNDRY_PROFILE: ci
31-
INFURA_ID: ${{ secrets.INFURA_ID }}
3231
WALLET_CONNECT_PROJECT_ID: ${{ secrets.WALLET_CONNECT_PROJECT_ID }}
3332

3433
jobs:

contracts-core/.env.example

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
INFURA_ID=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
21
MNEMONIC=here is where your twelve words mnemonic should be put my friend
32
DEPLOY_GSN=false
43
ETHERSCAN_VERIFICATION_API_KEY="YOUR_API_KEY"

contracts-periphery/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ install :; $(INSTALL_CMD)
1313
test :; forge test --sender 0x4f78F7f3482D9f1790649f9DD18Eec5A1Cc70F86 --no-match-contract ApproveBatchSendTokensTest
1414
test-gas :; forge test --match-path *.gas.t.sol
1515
snapshot-gas :; forge test --match-path *.gas.t.sol --gas-report > snapshot/.gas
16-
coverage :; forge coverage --report lcov --report summary && sed -i'.bak' 's/SF:/SF:contracts-periphery\//gI' lcov.info
16+
coverage :; forge coverage --sender 0x4f78F7f3482D9f1790649f9DD18Eec5A1Cc70F86 --report lcov --report summary && sed -i'.bak' 's/SF:/SF:contracts-periphery\//gI' lcov.info

contracts-periphery/script/ApproveBatchSendTokens.s.sol

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import {UmbraBatchSend} from "src/UmbraBatchSend.sol";
77

88
contract ApproveBatchSendTokens is Script {
99
function run(
10+
address _owner,
1011
address _umbraContractAddress,
1112
address _batchSendContractAddress,
1213
address[] calldata _tokenAddressesToApprove
1314
) public {
14-
vm.startBroadcast();
15+
vm.startBroadcast(_owner);
1516
for (uint256 _i = 0; _i < _tokenAddressesToApprove.length; _i++) {
1617
uint256 _currentAllowance = IERC20(_tokenAddressesToApprove[_i]).allowance(
1718
_batchSendContractAddress, _umbraContractAddress

contracts-periphery/script/DeployBatchSend.s.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ contract DeployBatchSend is Script {
3030
/// @notice Deploy the contract to the list of networks,
3131
function run() public {
3232
// Compute the address the contract will be deployed to
33-
address expectedContractAddress = computeCreateAddress(msg.sender, EXPECTED_NONCE);
33+
address expectedContractAddress = vm.computeCreateAddress(msg.sender, EXPECTED_NONCE);
3434
console2.log("Expected contract address: %s", expectedContractAddress);
3535

3636
// Turn off fallback to default RPC URLs since they can be flaky.

contracts-periphery/test/ApproveBatchSendTokens.t.sol

+6-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ contract ApproveBatchSendTokensTest is Test {
1717
address constant WBTC_ADDRESS = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
1818
address[] tokensToApprove =
1919
[DAI_ADDRESS, LUSD_ADDRESS, RAI_ADDRESS, USDC_ADDRESS, USDT_ADDRESS, WBTC_ADDRESS];
20+
address owner = 0xB7EE870E2c49B2DEEe70003519cF056247Aac3D4;
2021

2122
function setUp() public {
2223
vm.createSelectFork(vm.rpcUrl("mainnet"), 18_428_858);
@@ -27,7 +28,10 @@ contract ApproveBatchSendTokensTest is Test {
2728
address[] memory tokenAddressesToApprove = new address[](1);
2829
tokenAddressesToApprove[0] = DAI_ADDRESS;
2930
approveTokensScript.run(
30-
umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokenAddressesToApprove
31+
owner,
32+
umbraContractAddressOnMainnet,
33+
batchSendContractAddressOnMainnet,
34+
tokenAddressesToApprove
3135
);
3236

3337
assertEq(
@@ -40,7 +44,7 @@ contract ApproveBatchSendTokensTest is Test {
4044

4145
function test_ApproveMultipleTokens() public {
4246
approveTokensScript.run(
43-
umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokensToApprove
47+
owner, umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokensToApprove
4448
);
4549

4650
for (uint256 _i; _i < tokensToApprove.length; _i++) {

contracts-periphery/test/DeployBatchSend.t.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ contract DeployBatchSendTest is DeployBatchSend, Test {
1414
bytes batchSendCode;
1515

1616
function setUp() public {
17-
expectedContractAddress = computeCreateAddress(sender, EXPECTED_NONCE);
17+
expectedContractAddress = vm.computeCreateAddress(sender, EXPECTED_NONCE);
1818
umbraBatchSendTest = new UmbraBatchSend(IUmbra(UMBRA));
1919
batchSendCode = address(umbraBatchSendTest).code;
2020
}

contracts-periphery/test/UmbraBatchSend.t.sol

+14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ abstract contract UmbraBatchSendTest is DeployUmbraTest {
2020
error NotSorted();
2121
error TooMuchEthSent();
2222

23+
function _sortSendDataByToken(UmbraBatchSend.SendData[] storage arr) internal {
24+
for (uint256 i = 0; i < arr.length - 1; i++) {
25+
for (uint256 j = 0; j < arr.length - i - 1; j++) {
26+
if (arr[j].tokenAddr > arr[j + 1].tokenAddr) {
27+
UmbraBatchSend.SendData memory temp = arr[j];
28+
arr[j] = arr[j + 1];
29+
arr[j + 1] = temp;
30+
}
31+
}
32+
}
33+
}
34+
2335
function setUp() public virtual override {
2436
super.setUp();
2537
router = new UmbraBatchSend(IUmbra(address(umbra)));
@@ -94,6 +106,8 @@ abstract contract UmbraBatchSendTest is DeployUmbraTest {
94106
sendData.push(UmbraBatchSend.SendData(alice, address(token), amount, pkx, ciphertext));
95107
sendData.push(UmbraBatchSend.SendData(bob, address(token), amount2, pkx, ciphertext));
96108

109+
_sortSendDataByToken(sendData);
110+
97111
uint256 totalToll = toll * sendData.length;
98112
token.approve(address(router), totalAmount);
99113
token2.approve(address(router), totalAmount2);

frontend/.env.example

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ OPTIMISTIC_ETHERSCAN_API_KEY=yourOptimisticEtherscanApiKey
33
POLYGONSCAN_API_KEY=yourPolygonscanApiKey
44
ARBISCAN_API_KEY=yourArbiscanApiKey
55

6-
INFURA_ID=yourKeyHere
76
BLOCKNATIVE_API_KEY=yourKeyHere
87
FORTMATIC_API_KEY=yourKeyHere
98
PORTIS_API_KEY=yourKeyHere

frontend/src/components/AccountReceiveTable.vue

+83-16
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,45 @@
6060
>.
6161
</div>
6262

63-
<div v-if="scanStatus === 'complete'" class="text-caption q-mb-sm">
64-
<!-- Show the most recent timestamp and block that were scanned -->
65-
{{ $t('AccountReceiveTable.most-recent-announcement') }}
66-
{{ mostRecentAnnouncementBlockNumber }} /
67-
{{ formatDate(mostRecentAnnouncementTimestamp * 1000) }}
68-
{{ formatTime(mostRecentAnnouncementTimestamp * 1000) }}
63+
<div v-if="mostRecentAnnouncementBlockNumber && mostRecentAnnouncementTimestamp" class="text-caption q-mb-md">
64+
<!-- Container for block data and fetching status -->
65+
<div class="block-data-container row items-center justify-between q-col-gutter-md">
66+
<!-- Block data -->
67+
<div class="block-data">
68+
{{ $t('AccountReceiveTable.most-recent-announcement') }}
69+
{{ mostRecentAnnouncementBlockNumber }} /
70+
{{ formatDate(mostRecentAnnouncementTimestamp * 1000) }}
71+
{{ formatTime(mostRecentAnnouncementTimestamp * 1000) }}
72+
</div>
73+
74+
<!-- Status messages -->
75+
<div
76+
v-if="
77+
['fetching', 'fetching latest', 'scanning', 'scanning latest from last fetched block'].includes(
78+
scanStatus
79+
)
80+
"
81+
class="status-message text-italic"
82+
>
83+
<div v-if="scanStatus === 'fetching' || scanStatus === 'fetching latest'">
84+
{{
85+
scanStatus === 'fetching'
86+
? $t('Receive.fetching')
87+
: $t('Receive.fetching-latest-from-last-fetched-block')
88+
}}
89+
<q-spinner-dots color="primary" size="1em" class="q-ml-xs" />
90+
</div>
91+
<div v-else>
92+
{{
93+
scanStatus === 'scanning latest from last fetched block'
94+
? $t('Receive.scanning-latest-from-last-fetched-block')
95+
: $t('Receive.scanning')
96+
}}
97+
<q-spinner-dots color="primary" size="1em" class="q-ml-xs" />
98+
</div>
99+
</div>
100+
</div>
101+
69102
<div v-if="advancedMode" class="text-caption q-mb-sm">
70103
{{ $t('AccountReceiveTable.most-recent-mined') }}
71104
{{ mostRecentBlockNumber }} /
@@ -385,7 +418,7 @@
385418
</template>
386419

387420
<script lang="ts">
388-
import { computed, defineComponent, watch, PropType, ref, watchEffect, Ref } from 'vue';
421+
import { computed, defineComponent, watch, PropType, ref, watchEffect, Ref, ComputedRef, onMounted } from 'vue';
389422
import { copyToClipboard } from 'quasar';
390423
import { BigNumber, Contract, joinSignature, formatUnits, TransactionResponse, Web3Provider } from 'src/utils/ethers';
391424
import { Umbra, UserAnnouncement, KeyPair, utils } from '@umbracash/umbra-js';
@@ -461,7 +494,7 @@ interface ReceiveTableAnnouncement extends UserAnnouncement {
461494
formattedFrom: string;
462495
}
463496
464-
function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spendingKeyPair: KeyPair) {
497+
function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spendingKeyPair: ComputedRef<KeyPair>) {
465498
const { NATIVE_TOKEN, network, provider, signer, umbra, userAddress, relayer, tokens } = useWalletStore();
466499
const { setIsInWithdrawFlow } = useStatusesStore();
467500
const paginationConfig = { rowsPerPage: 25 };
@@ -542,13 +575,17 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
542575
// Format announcements so from addresses support ENS/CNS, and so we can easily detect withdrawals
543576
const formattedAnnouncements = ref([] as ReceiveTableAnnouncement[]);
544577
578+
const sortByTimestamp = (announcements: ReceiveTableAnnouncement[]) =>
579+
announcements.sort((a, b) => Number(b.timestamp) - Number(a.timestamp));
580+
545581
// eslint-disable-next-line @typescript-eslint/no-misused-promises
546582
watchEffect(async () => {
547-
if (userAnnouncements.value.length === 0) formattedAnnouncements.value = [];
548-
isLoading.value = true;
583+
const hasAnnouncements = userAnnouncements.value.length > 0;
584+
if (!hasAnnouncements) formattedAnnouncements.value = [];
585+
isLoading.value = !hasAnnouncements;
549586
const announcements = userAnnouncements.value as ReceiveTableAnnouncement[];
550587
const newAnnouncements = announcements.filter((x) => !formattedAnnouncements.value.includes(x));
551-
formattedAnnouncements.value = [...formattedAnnouncements.value, ...newAnnouncements];
588+
formattedAnnouncements.value = sortByTimestamp([...formattedAnnouncements.value, ...newAnnouncements]);
552589
// Format addresses to use ENS, CNS, or formatted address
553590
const fromAddresses = announcements.map((announcement) => announcement.from);
554591
let formattedAddresses: string[] = [];
@@ -584,9 +621,19 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
584621
const stealthBalanceResponses: Response[] = await multicall.callStatic.aggregate3(stealthBalanceCalls);
585622
const stealthBalances = stealthBalanceResponses.map((r) => BigNumber.from(r.returnData));
586623
587-
formattedAnnouncements.value.forEach((announcement, index) => {
588-
if (newAnnouncements.some((newAnnouncement) => newAnnouncement.txHash === announcement.txHash))
589-
announcement.isWithdrawn = stealthBalances[index].lt(announcement.amount);
624+
formattedAnnouncements.value.forEach((announcement) => {
625+
const isNewAnnouncement = newAnnouncements.some(
626+
(newAnnouncement) =>
627+
newAnnouncement.txHash === announcement.txHash && newAnnouncement.receiver === announcement.receiver
628+
);
629+
630+
if (isNewAnnouncement) {
631+
const balanceIndex = userAnnouncements.value.findIndex(
632+
(a) => a.txHash === announcement.txHash && a.receiver === announcement.receiver
633+
);
634+
const stealthBalance = stealthBalances[balanceIndex];
635+
announcement.isWithdrawn = stealthBalance.lt(announcement.amount);
636+
}
590637
});
591638
isLoading.value = false;
592639
});
@@ -676,7 +723,7 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
676723
// Get token info, stealth private key, and destination (acceptor) address
677724
const announcement = activeAnnouncement.value;
678725
const token = getTokenInfo(announcement.token);
679-
const stealthKeyPair = spendingKeyPair.mulPrivateKey(announcement.randomNumber);
726+
const stealthKeyPair = spendingKeyPair.value.mulPrivateKey(announcement.randomNumber);
680727
const spendingPrivateKey = stealthKeyPair.privateKeyHex as string;
681728
const acceptor = await toAddress(destinationAddress.value, provider.value);
682729
await utils.assertSupportedAddress(acceptor);
@@ -837,6 +884,10 @@ export default defineComponent({
837884
}
838885
);
839886
887+
onMounted(() => {
888+
setIsInWithdrawFlow(false);
889+
});
890+
840891
return {
841892
advancedMode,
842893
context,
@@ -848,7 +899,7 @@ export default defineComponent({
848899
userAnnouncements,
849900
setIsInWithdrawFlow,
850901
...useAdvancedFeatures(spendingKeyPair.value),
851-
...useReceivedFundsTable(userAnnouncements, spendingKeyPair.value),
902+
...useReceivedFundsTable(userAnnouncements, spendingKeyPair),
852903
};
853904
},
854905
});
@@ -869,4 +920,20 @@ export default defineComponent({
869920
870921
.external-link-icon
871922
color: transparent
923+
924+
.block-data-container
925+
@media (max-width: 599px)
926+
flex-direction: column
927+
align-items: flex-start
928+
929+
@media (min-width: 600px)
930+
flex-direction: row
931+
932+
.block-data, .fetching-status
933+
@media (max-width: 599px)
934+
width: 100%
935+
936+
.fetching-status
937+
@media (max-width: 599px)
938+
margin-top: 0.5rem
872939
</style>

frontend/src/components/WithdrawForm.vue

+44-12
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,14 @@
1414
<base-input
1515
v-model="content"
1616
@update:modelValue="emitUpdateDestinationAddress"
17-
@click="
18-
emit('initializeWithdraw');
19-
setIsInWithdrawFlow(true);
20-
"
21-
:appendButtonLabel="$t('WithdrawForm.withdraw')"
22-
:appendButtonDisable="isInWithdrawFlow || isFeeLoading"
17+
@click="handleSubmit"
18+
:appendButtonLabel="needSignature ? $t('WithdrawForm.need-signature') : $t('WithdrawForm.withdraw')"
19+
:appendButtonDisable="isInWithdrawFlow || isFeeLoading || isSigningInProgress"
2320
:appendButtonLoading="isInWithdrawFlow"
2421
:disable="isInWithdrawFlow"
2522
:label="$t('WithdrawForm.address')"
2623
lazy-rules
27-
:rules="(val) => (val && val.length > 4) || $t('WithdrawForm.enter-valid-address')"
24+
:rules="(val: string | null) => (val && val.length > 4) || $t('WithdrawForm.enter-valid-address')"
2825
/>
2926
<!-- Fee estimate -->
3027
<div class="q-mb-lg">
@@ -119,26 +116,61 @@ export default defineComponent({
119116
advancedMode: {
120117
type: Boolean,
121118
required: true,
119+
default: true,
122120
},
123121
},
124122
setup(data, { emit }) {
125-
const { NATIVE_TOKEN } = useWalletStore();
123+
const { NATIVE_TOKEN, getPrivateKeys } = useWalletStore();
126124
const { setIsInWithdrawFlow, isInWithdrawFlow } = useStatusesStore();
125+
const { needSignature } = useWalletStore();
127126
const content = ref<string>(data.destinationAddress || '');
128127
const nativeTokenSymbol = NATIVE_TOKEN.value.symbol;
128+
const isSigningInProgress = ref(false);
129129
130130
function emitUpdateDestinationAddress(val: string) {
131131
emit('updateDestinationAddress', val);
132132
}
133133
134+
function initializeWithdraw() {
135+
// Simple validation
136+
if (!content.value || content.value.length <= 4) return;
137+
138+
emit('initializeWithdraw');
139+
setIsInWithdrawFlow(true);
140+
}
141+
142+
async function handleSubmit() {
143+
if (needSignature.value) {
144+
try {
145+
isSigningInProgress.value = true;
146+
const success = await getPrivateKeys();
147+
if (success === 'denied') {
148+
console.log('User denied signature request');
149+
isSigningInProgress.value = false;
150+
return;
151+
}
152+
initializeWithdraw();
153+
} catch (error) {
154+
console.error('Error getting private keys:', error);
155+
} finally {
156+
isSigningInProgress.value = false;
157+
}
158+
} else {
159+
initializeWithdraw();
160+
}
161+
}
162+
134163
return {
135-
formatUnits,
136-
humanizeTokenAmount,
164+
content,
137165
emit,
138166
emitUpdateDestinationAddress,
139-
content,
140-
nativeTokenSymbol,
167+
formatUnits,
168+
handleSubmit,
169+
humanizeTokenAmount,
141170
isInWithdrawFlow,
171+
isSigningInProgress,
172+
nativeTokenSymbol,
173+
needSignature,
142174
setIsInWithdrawFlow,
143175
};
144176
},

0 commit comments

Comments
 (0)