Skip to content

Commit

Permalink
feat: save user announcements to local storage and last fetched block (
Browse files Browse the repository at this point in the history
…ScopeLift#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 <61768337+garyghayrat@users.noreply.github.com>

* feat: handle sign button instead of needs sig

* Update frontend/src/i18n/locales/zh-CN.json

Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com>

* 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 (ScopeLift#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 (ScopeLift#687)

---------

Co-authored-by: John Feras <jferas@ferasinfotech.com>

* 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 <61768337+garyghayrat@users.noreply.github.com>
Co-authored-by: John Feras <jferas@ferasinfotech.com>
  • Loading branch information
3 people authored and OKEAMAH committed Jul 11, 2024
1 parent d2be41c commit 0ea66cc
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 104 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ env:
GNOSIS_CHAIN_RPC_URL: ${{ secrets.GNOSIS_CHAIN_RPC_URL }}
BASE_RPC_URL: $${{ secrets.BASE_RPC_URL }}
FOUNDRY_PROFILE: ci
INFURA_ID: ${{ secrets.INFURA_ID }}
WALLET_CONNECT_PROJECT_ID: ${{ secrets.WALLET_CONNECT_PROJECT_ID }}

jobs:
Expand Down
1 change: 0 additions & 1 deletion contracts-core/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
INFURA_ID=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
MNEMONIC=here is where your twelve words mnemonic should be put my friend
DEPLOY_GSN=false
ETHERSCAN_VERIFICATION_API_KEY="YOUR_API_KEY"
Expand Down
2 changes: 1 addition & 1 deletion contracts-periphery/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ install :; $(INSTALL_CMD)
test :; forge test --sender 0x4f78F7f3482D9f1790649f9DD18Eec5A1Cc70F86 --no-match-contract ApproveBatchSendTokensTest
test-gas :; forge test --match-path *.gas.t.sol
snapshot-gas :; forge test --match-path *.gas.t.sol --gas-report > snapshot/.gas
coverage :; forge coverage --report lcov --report summary && sed -i'.bak' 's/SF:/SF:contracts-periphery\//gI' lcov.info
coverage :; forge coverage --sender 0x4f78F7f3482D9f1790649f9DD18Eec5A1Cc70F86 --report lcov --report summary && sed -i'.bak' 's/SF:/SF:contracts-periphery\//gI' lcov.info
3 changes: 2 additions & 1 deletion contracts-periphery/script/ApproveBatchSendTokens.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {UmbraBatchSend} from "src/UmbraBatchSend.sol";

contract ApproveBatchSendTokens is Script {
function run(
address _owner,
address _umbraContractAddress,
address _batchSendContractAddress,
address[] calldata _tokenAddressesToApprove
) public {
vm.startBroadcast();
vm.startBroadcast(_owner);
for (uint256 _i = 0; _i < _tokenAddressesToApprove.length; _i++) {
uint256 _currentAllowance = IERC20(_tokenAddressesToApprove[_i]).allowance(
_batchSendContractAddress, _umbraContractAddress
Expand Down
2 changes: 1 addition & 1 deletion contracts-periphery/script/DeployBatchSend.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract DeployBatchSend is Script {
/// @notice Deploy the contract to the list of networks,
function run() public {
// Compute the address the contract will be deployed to
address expectedContractAddress = computeCreateAddress(msg.sender, EXPECTED_NONCE);
address expectedContractAddress = vm.computeCreateAddress(msg.sender, EXPECTED_NONCE);
console2.log("Expected contract address: %s", expectedContractAddress);

// Turn off fallback to default RPC URLs since they can be flaky.
Expand Down
8 changes: 6 additions & 2 deletions contracts-periphery/test/ApproveBatchSendTokens.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ contract ApproveBatchSendTokensTest is Test {
address constant WBTC_ADDRESS = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
address[] tokensToApprove =
[DAI_ADDRESS, LUSD_ADDRESS, RAI_ADDRESS, USDC_ADDRESS, USDT_ADDRESS, WBTC_ADDRESS];
address owner = 0xB7EE870E2c49B2DEEe70003519cF056247Aac3D4;

function setUp() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 18_428_858);
Expand All @@ -27,7 +28,10 @@ contract ApproveBatchSendTokensTest is Test {
address[] memory tokenAddressesToApprove = new address[](1);
tokenAddressesToApprove[0] = DAI_ADDRESS;
approveTokensScript.run(
umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokenAddressesToApprove
owner,
umbraContractAddressOnMainnet,
batchSendContractAddressOnMainnet,
tokenAddressesToApprove
);

assertEq(
Expand All @@ -40,7 +44,7 @@ contract ApproveBatchSendTokensTest is Test {

function test_ApproveMultipleTokens() public {
approveTokensScript.run(
umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokensToApprove
owner, umbraContractAddressOnMainnet, batchSendContractAddressOnMainnet, tokensToApprove
);

for (uint256 _i; _i < tokensToApprove.length; _i++) {
Expand Down
2 changes: 1 addition & 1 deletion contracts-periphery/test/DeployBatchSend.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contract DeployBatchSendTest is DeployBatchSend, Test {
bytes batchSendCode;

function setUp() public {
expectedContractAddress = computeCreateAddress(sender, EXPECTED_NONCE);
expectedContractAddress = vm.computeCreateAddress(sender, EXPECTED_NONCE);
umbraBatchSendTest = new UmbraBatchSend(IUmbra(UMBRA));
batchSendCode = address(umbraBatchSendTest).code;
}
Expand Down
14 changes: 14 additions & 0 deletions contracts-periphery/test/UmbraBatchSend.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ abstract contract UmbraBatchSendTest is DeployUmbraTest {
error NotSorted();
error TooMuchEthSent();

function _sortSendDataByToken(UmbraBatchSend.SendData[] storage arr) internal {
for (uint256 i = 0; i < arr.length - 1; i++) {
for (uint256 j = 0; j < arr.length - i - 1; j++) {
if (arr[j].tokenAddr > arr[j + 1].tokenAddr) {
UmbraBatchSend.SendData memory temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

function setUp() public virtual override {
super.setUp();
router = new UmbraBatchSend(IUmbra(address(umbra)));
Expand Down Expand Up @@ -94,6 +106,8 @@ abstract contract UmbraBatchSendTest is DeployUmbraTest {
sendData.push(UmbraBatchSend.SendData(alice, address(token), amount, pkx, ciphertext));
sendData.push(UmbraBatchSend.SendData(bob, address(token), amount2, pkx, ciphertext));

_sortSendDataByToken(sendData);

uint256 totalToll = toll * sendData.length;
token.approve(address(router), totalAmount);
token2.approve(address(router), totalAmount2);
Expand Down
1 change: 0 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ OPTIMISTIC_ETHERSCAN_API_KEY=yourOptimisticEtherscanApiKey
POLYGONSCAN_API_KEY=yourPolygonscanApiKey
ARBISCAN_API_KEY=yourArbiscanApiKey

INFURA_ID=yourKeyHere
BLOCKNATIVE_API_KEY=yourKeyHere
FORTMATIC_API_KEY=yourKeyHere
PORTIS_API_KEY=yourKeyHere
Expand Down
99 changes: 83 additions & 16 deletions frontend/src/components/AccountReceiveTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,45 @@
>.
</div>

<div v-if="scanStatus === 'complete'" class="text-caption q-mb-sm">
<!-- Show the most recent timestamp and block that were scanned -->
{{ $t('AccountReceiveTable.most-recent-announcement') }}
{{ mostRecentAnnouncementBlockNumber }} /
{{ formatDate(mostRecentAnnouncementTimestamp * 1000) }}
{{ formatTime(mostRecentAnnouncementTimestamp * 1000) }}
<div v-if="mostRecentAnnouncementBlockNumber && mostRecentAnnouncementTimestamp" class="text-caption q-mb-md">
<!-- Container for block data and fetching status -->
<div class="block-data-container row items-center justify-between q-col-gutter-md">
<!-- Block data -->
<div class="block-data">
{{ $t('AccountReceiveTable.most-recent-announcement') }}
{{ mostRecentAnnouncementBlockNumber }} /
{{ formatDate(mostRecentAnnouncementTimestamp * 1000) }}
{{ formatTime(mostRecentAnnouncementTimestamp * 1000) }}
</div>

<!-- Status messages -->
<div
v-if="
['fetching', 'fetching latest', 'scanning', 'scanning latest from last fetched block'].includes(
scanStatus
)
"
class="status-message text-italic"
>
<div v-if="scanStatus === 'fetching' || scanStatus === 'fetching latest'">
{{
scanStatus === 'fetching'
? $t('Receive.fetching')
: $t('Receive.fetching-latest-from-last-fetched-block')
}}
<q-spinner-dots color="primary" size="1em" class="q-ml-xs" />
</div>
<div v-else>
{{
scanStatus === 'scanning latest from last fetched block'
? $t('Receive.scanning-latest-from-last-fetched-block')
: $t('Receive.scanning')
}}
<q-spinner-dots color="primary" size="1em" class="q-ml-xs" />
</div>
</div>
</div>

<div v-if="advancedMode" class="text-caption q-mb-sm">
{{ $t('AccountReceiveTable.most-recent-mined') }}
{{ mostRecentBlockNumber }} /
Expand Down Expand Up @@ -385,7 +418,7 @@
</template>

<script lang="ts">
import { computed, defineComponent, watch, PropType, ref, watchEffect, Ref } from 'vue';
import { computed, defineComponent, watch, PropType, ref, watchEffect, Ref, ComputedRef, onMounted } from 'vue';
import { copyToClipboard } from 'quasar';
import { BigNumber, Contract, joinSignature, formatUnits, TransactionResponse, Web3Provider } from 'src/utils/ethers';
import { Umbra, UserAnnouncement, KeyPair, utils } from '@umbracash/umbra-js';
Expand Down Expand Up @@ -461,7 +494,7 @@ interface ReceiveTableAnnouncement extends UserAnnouncement {
formattedFrom: string;
}
function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spendingKeyPair: KeyPair) {
function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spendingKeyPair: ComputedRef<KeyPair>) {
const { NATIVE_TOKEN, network, provider, signer, umbra, userAddress, relayer, tokens } = useWalletStore();
const { setIsInWithdrawFlow } = useStatusesStore();
const paginationConfig = { rowsPerPage: 25 };
Expand Down Expand Up @@ -542,13 +575,17 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
// Format announcements so from addresses support ENS/CNS, and so we can easily detect withdrawals
const formattedAnnouncements = ref([] as ReceiveTableAnnouncement[]);
const sortByTimestamp = (announcements: ReceiveTableAnnouncement[]) =>
announcements.sort((a, b) => Number(b.timestamp) - Number(a.timestamp));
// eslint-disable-next-line @typescript-eslint/no-misused-promises
watchEffect(async () => {
if (userAnnouncements.value.length === 0) formattedAnnouncements.value = [];
isLoading.value = true;
const hasAnnouncements = userAnnouncements.value.length > 0;
if (!hasAnnouncements) formattedAnnouncements.value = [];
isLoading.value = !hasAnnouncements;
const announcements = userAnnouncements.value as ReceiveTableAnnouncement[];
const newAnnouncements = announcements.filter((x) => !formattedAnnouncements.value.includes(x));
formattedAnnouncements.value = [...formattedAnnouncements.value, ...newAnnouncements];
formattedAnnouncements.value = sortByTimestamp([...formattedAnnouncements.value, ...newAnnouncements]);
// Format addresses to use ENS, CNS, or formatted address
const fromAddresses = announcements.map((announcement) => announcement.from);
let formattedAddresses: string[] = [];
Expand Down Expand Up @@ -584,9 +621,19 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
const stealthBalanceResponses: Response[] = await multicall.callStatic.aggregate3(stealthBalanceCalls);
const stealthBalances = stealthBalanceResponses.map((r) => BigNumber.from(r.returnData));
formattedAnnouncements.value.forEach((announcement, index) => {
if (newAnnouncements.some((newAnnouncement) => newAnnouncement.txHash === announcement.txHash))
announcement.isWithdrawn = stealthBalances[index].lt(announcement.amount);
formattedAnnouncements.value.forEach((announcement) => {
const isNewAnnouncement = newAnnouncements.some(
(newAnnouncement) =>
newAnnouncement.txHash === announcement.txHash && newAnnouncement.receiver === announcement.receiver
);
if (isNewAnnouncement) {
const balanceIndex = userAnnouncements.value.findIndex(
(a) => a.txHash === announcement.txHash && a.receiver === announcement.receiver
);
const stealthBalance = stealthBalances[balanceIndex];
announcement.isWithdrawn = stealthBalance.lt(announcement.amount);
}
});
isLoading.value = false;
});
Expand Down Expand Up @@ -676,7 +723,7 @@ function useReceivedFundsTable(userAnnouncements: Ref<UserAnnouncement[]>, spend
// Get token info, stealth private key, and destination (acceptor) address
const announcement = activeAnnouncement.value;
const token = getTokenInfo(announcement.token);
const stealthKeyPair = spendingKeyPair.mulPrivateKey(announcement.randomNumber);
const stealthKeyPair = spendingKeyPair.value.mulPrivateKey(announcement.randomNumber);
const spendingPrivateKey = stealthKeyPair.privateKeyHex as string;
const acceptor = await toAddress(destinationAddress.value, provider.value);
await utils.assertSupportedAddress(acceptor);
Expand Down Expand Up @@ -837,6 +884,10 @@ export default defineComponent({
}
);
onMounted(() => {
setIsInWithdrawFlow(false);
});
return {
advancedMode,
context,
Expand All @@ -848,7 +899,7 @@ export default defineComponent({
userAnnouncements,
setIsInWithdrawFlow,
...useAdvancedFeatures(spendingKeyPair.value),
...useReceivedFundsTable(userAnnouncements, spendingKeyPair.value),
...useReceivedFundsTable(userAnnouncements, spendingKeyPair),
};
},
});
Expand All @@ -869,4 +920,20 @@ export default defineComponent({
.external-link-icon
color: transparent
.block-data-container
@media (max-width: 599px)
flex-direction: column
align-items: flex-start
@media (min-width: 600px)
flex-direction: row
.block-data, .fetching-status
@media (max-width: 599px)
width: 100%
.fetching-status
@media (max-width: 599px)
margin-top: 0.5rem
</style>
56 changes: 44 additions & 12 deletions frontend/src/components/WithdrawForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@
<base-input
v-model="content"
@update:modelValue="emitUpdateDestinationAddress"
@click="
emit('initializeWithdraw');
setIsInWithdrawFlow(true);
"
:appendButtonLabel="$t('WithdrawForm.withdraw')"
:appendButtonDisable="isInWithdrawFlow || isFeeLoading"
@click="handleSubmit"
:appendButtonLabel="needSignature ? $t('WithdrawForm.need-signature') : $t('WithdrawForm.withdraw')"
:appendButtonDisable="isInWithdrawFlow || isFeeLoading || isSigningInProgress"
:appendButtonLoading="isInWithdrawFlow"
:disable="isInWithdrawFlow"
:label="$t('WithdrawForm.address')"
lazy-rules
:rules="(val) => (val && val.length > 4) || $t('WithdrawForm.enter-valid-address')"
:rules="(val: string | null) => (val && val.length > 4) || $t('WithdrawForm.enter-valid-address')"
/>
<!-- Fee estimate -->
<div class="q-mb-lg">
Expand Down Expand Up @@ -119,26 +116,61 @@ export default defineComponent({
advancedMode: {
type: Boolean,
required: true,
default: true,
},
},
setup(data, { emit }) {
const { NATIVE_TOKEN } = useWalletStore();
const { NATIVE_TOKEN, getPrivateKeys } = useWalletStore();
const { setIsInWithdrawFlow, isInWithdrawFlow } = useStatusesStore();
const { needSignature } = useWalletStore();
const content = ref<string>(data.destinationAddress || '');
const nativeTokenSymbol = NATIVE_TOKEN.value.symbol;
const isSigningInProgress = ref(false);
function emitUpdateDestinationAddress(val: string) {
emit('updateDestinationAddress', val);
}
function initializeWithdraw() {
// Simple validation
if (!content.value || content.value.length <= 4) return;
emit('initializeWithdraw');
setIsInWithdrawFlow(true);
}
async function handleSubmit() {
if (needSignature.value) {
try {
isSigningInProgress.value = true;
const success = await getPrivateKeys();
if (success === 'denied') {
console.log('User denied signature request');
isSigningInProgress.value = false;
return;
}
initializeWithdraw();
} catch (error) {
console.error('Error getting private keys:', error);
} finally {
isSigningInProgress.value = false;
}
} else {
initializeWithdraw();
}
}
return {
formatUnits,
humanizeTokenAmount,
content,
emit,
emitUpdateDestinationAddress,
content,
nativeTokenSymbol,
formatUnits,
handleSubmit,
humanizeTokenAmount,
isInWithdrawFlow,
isSigningInProgress,
nativeTokenSymbol,
needSignature,
setIsInWithdrawFlow,
};
},
Expand Down
Loading

0 comments on commit 0ea66cc

Please sign in to comment.