Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: save user announcements to local storage and last fetched block #685

Merged
merged 38 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6925fc0
feat: set the last fetched block as the start block
marcomariscal Apr 26, 2024
9932c07
feat: handle caching user announcements and latest fetched block
marcomariscal Apr 30, 2024
154ff46
feat: show user announcements if there are any
marcomariscal Apr 30, 2024
136c4f3
fix: handle watching/loading announcements
marcomariscal Apr 30, 2024
9ba3115
fix: parse out lastFetchedBlock and fix user announcement loading logic
marcomariscal May 1, 2024
9b49317
chore: log
marcomariscal May 1, 2024
9af424d
feat: handle block data caching
marcomariscal Jun 20, 2024
c449a82
feat: show most recent block data if exists
marcomariscal Jun 20, 2024
22cc126
fix: type check
marcomariscal Jun 20, 2024
be0f63e
feat: handle user announcements already present and sign language
marcomariscal Jun 21, 2024
cd9e394
feat: only show fetching when no user announcements
marcomariscal Jun 24, 2024
1f9591d
feat: fetching latest from last fetched block component
marcomariscal Jun 24, 2024
6916958
feat: fetching latest translation for cn
marcomariscal Jun 24, 2024
8cbf9b0
feat: clear local storage button and functionality
marcomariscal Jun 24, 2024
5d04a36
fix: start block handling logic
marcomariscal Jun 24, 2024
def9921
feat: dedupe user announcements
marcomariscal Jun 24, 2024
3046be2
fix: logic
marcomariscal Jun 24, 2024
1c43d4d
fix: minimize debugging logs on userAnnouncement changes
marcomariscal Jun 25, 2024
c5ad0bb
feat: handle scanning latest announcements from last fetched block
marcomariscal Jun 25, 2024
5909fb8
feat: sort by timestamp explicitly
marcomariscal Jun 25, 2024
9f0a3f9
feat: no loading sequence when there are announcements
marcomariscal Jun 25, 2024
c40d035
fix: need sig lately verbiage
marcomariscal Jun 27, 2024
8a90bc7
fix: add need sig lately to cn
marcomariscal Jun 27, 2024
f39c807
fix: little more mb
marcomariscal Jun 27, 2024
aca5dc5
fix: no withdraw verbiage on need-sig-lately
marcomariscal Jun 28, 2024
29f6f6e
feat: handle need sig
marcomariscal Jun 28, 2024
44c53b3
Update frontend/src/i18n/locales/en-US.json
marcomariscal Jul 8, 2024
a211d46
feat: handle sign button instead of needs sig
marcomariscal Jul 8, 2024
e39a690
Update frontend/src/i18n/locales/zh-CN.json
marcomariscal Jul 8, 2024
ba09324
fix: move local storage clear button above lang
marcomariscal Jul 8, 2024
a72ff26
fix: spacing more uniform
marcomariscal Jul 9, 2024
d8b730b
fix: use computed ref as param, and set setIsInWithdrawFlow to false …
marcomariscal Jul 9, 2024
5d63f2a
feat: sign and withdraw
marcomariscal Jul 9, 2024
dcd0ab2
fix: contract periphery tests (#688)
marcomariscal Jul 9, 2024
af15d8c
fix: use balanceIndex to ensure that the correct balance is fetched f…
marcomariscal Jul 9, 2024
c6d04ed
fix: dedupe by tx hash and receiver instead of just tx hash
marcomariscal Jul 9, 2024
a6d8952
fix: include receiver to derive isWithdrawn
marcomariscal Jul 11, 2024
b627889
fix: img
marcomariscal Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
>.
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved
</div>

<div v-if="scanStatus === 'complete'" class="text-caption q-mb-sm">
<!-- Show the most recent timestamp and block that were scanned -->
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved
{{ $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
Loading