Skip to content

feat: snap birthday block form on install #67

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

Merged
merged 5 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion packages/snap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"test": "jest"
},
"dependencies": {
"@metamask/snaps-sdk": "^6.9.0",
"@metamask/snaps-sdk": "^6.17.1",
"@webzjs/webz-keys": "workspace:^",
"buffer": "^6.0.3",
"fork-ts-checker-webpack-plugin": "^9.0.2"
Expand All @@ -41,6 +41,8 @@
"@metamask/eslint-config-typescript": "^14.0.0",
"@metamask/snaps-cli": "^6.5.2",
"@metamask/snaps-jest": "^8.8.1",
"@types/react": "18.2.4",
"@types/react-dom ": "18.2.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^5.42.1",
"eslint": "^9.14.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/template-snap-monorepo.git"
},
"source": {
"shasum": "SA1fH7ay1ietGUsIELeD6rgpMuXu/rusR65jSdjCBrk=",
"shasum": "5kk+7sR8jY1fw3MjTH9dRIPkwAHHo0pFgfCseq+3rko=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand All @@ -29,5 +29,6 @@
}
]
},
"platformVersion": "6.17.1",
"manifestVersion": "0.1"
}
29 changes: 24 additions & 5 deletions packages/snap/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getViewingKey } from './rpc/getViewingKey';
import { InitOutput } from '@webzjs/webz-keys';
import { initialiseWasm } from './utils/initialiseWasm';
import { OnRpcRequestHandler, OnUserInputHandler, UserInputEventType } from '@metamask/snaps-sdk';
import { setBirthdayBlock, SetBirthdayBlockParams } from './rpc/setBirthdayBlock';

let wasm: InitOutput;

Expand All @@ -13,19 +15,36 @@ let wasm: InitOutput;
* @returns The ViewingKey
* @throws If the request method is not valid for this snap.
*/
export const onRpcRequest: ({
request,
}: {
request: any;
}) => Promise<string> = async ({ request }) => {
export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
if (!wasm) {
wasm = initialiseWasm();
}

switch (request.method) {
case 'getViewingKey':
return await getViewingKey();
case 'setBirthdayBlock':
const params = request.params as SetBirthdayBlockParams;
return await setBirthdayBlock(params);
default:
throw new Error('Method not found.');
}
};

export const onUserInput: OnUserInputHandler = async ({ id, event, context }) => {
if (event.type === UserInputEventType.FormSubmitEvent) {
switch (event.name) {
case "birthday-block-form":
await snap.request({
method: "snap_resolveInterface",
params: {
id,
value: event.value
}
})

default:
break;
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export async function getViewingKey(
const seed = await getSeed();

// Generate the UnifiedSpendingKey and obtain the Viewing Key
let spendingKey = new UnifiedSpendingKey(network, seed, accountIndex);
let viewingKey = spendingKey.to_unified_full_viewing_key();
const spendingKey = new UnifiedSpendingKey(network, seed, accountIndex);
const viewingKey = spendingKey.to_unified_full_viewing_key().encode(network);

return viewingKey.encode(network);
return viewingKey
} catch (error) {
console.error('Error generating Viewing Key:', error);
throw new Error('Failed to generate Viewing Key');
Expand Down
63 changes: 63 additions & 0 deletions packages/snap/src/rpc/setBirthdayBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Form,
Box,
Heading,
Input,
Button,
Text,
Bold,
Divider
} from '@metamask/snaps-sdk/jsx';

type BirthdayBlockForm = { customBirthdayBlock: string | null };

export type SetBirthdayBlockParams = { latestBlock?: number };

export async function setBirthdayBlock({
latestBlock,
}: SetBirthdayBlockParams): Promise<string | null> {

const interfaceId = await snap.request({
method: 'snap_createInterface',
params: {
ui: (
<Form name="birthday-block-form">
<Box>
<Heading>Optional syncing block height</Heading>
<Text>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be nice to give a user a hint about the current block height as a reference point.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guess I would need "endowment:network-access" and some API providing current block of the chain. Or did I miss some method in wasm returning latest block without initialized wallet?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can pass it from front-end when calling setBirthdayBlock()

If you alerady created Zcash Web Wallet account with this Metamask
seed you can enter optional birthday block of that Wallet.
</Text>
<Divider />
<Text>
Syncing
proccess will start from that block.
</Text>
<Divider />
{!!latestBlock && <Text>Latest block: <Bold>{latestBlock.toString()}</Bold></Text>}
<Input
min={0}
step={1}
type="number"
name="customBirthdayBlock"
placeholder="optional syncing block height"
/>
</Box>
<Button type="submit" name="next">
Continue to wallet
</Button>
</Form>
),
},
});

const { customBirthdayBlock } = (await snap.request({
method: 'snap_dialog',
params: {
id: interfaceId,
},
})) as BirthdayBlockForm;


return customBirthdayBlock;
}
1 change: 1 addition & 0 deletions packages/web-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"globals": "^15.11.0",
"parcel": "^2.13.3",
"path": "^0.12.7",
"svgo": "^3",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.11.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/web-wallet/src/context/WebzjsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface State {
error: Error | null | string;
summary?: WalletSummary;
chainHeight?: bigint;
activeAccount?: number;
activeAccount?: number | null;
syncInProgress: boolean;
loading: boolean;
}
Expand All @@ -42,7 +42,7 @@ const initialState: State = {
error: null,
summary: undefined,
chainHeight: undefined,
activeAccount: 0,
activeAccount: null,
syncInProgress: false,
loading: true,
};
Expand Down
76 changes: 63 additions & 13 deletions packages/web-wallet/src/hooks/useWebzjsActions.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { set } from 'idb-keyval';
import { useCallback } from 'react';
import { useWebZjsContext } from '../context/WebzjsContext';
import { useMetaMask } from './snaps/useMetaMask';
import { useInvokeSnap } from './snaps/useInvokeSnap';
import { useRequestSnap } from './snaps/useRequestSnap';

interface WebzjsActions {
addNewAccountFromUfvk: (
ufvk: string,
birthdayHeight: number,
) => Promise<void>;
getAccountData: () => Promise<
{ unifiedAddress: string; transparentAddress: string } | undefined
>;
triggerRescan: () => Promise<void>;
flushDbToStore: () => Promise<void>;
syncStateWithWallet: () => Promise<void>;
connectWebZjsSnap: () => Promise<void>;
}

export function useWebZjsActions(): WebzjsActions {
const { state, dispatch } = useWebZjsContext();
const { installedSnap } = useMetaMask();
const invokeSnap = useInvokeSnap();
const requestSnap = useRequestSnap();

const getAccountData = useCallback(async () => {
try {
Expand Down Expand Up @@ -81,23 +84,70 @@ export function useWebZjsActions(): WebzjsActions {
}
}, [state.webWallet, dispatch]);

const addNewAccountFromUfvk = useCallback(
async (ufvk: string, birthdayHeight: number) => {
const account_id =
(await state.webWallet?.create_account_ufvk(ufvk, birthdayHeight)) || 0;
dispatch({ type: 'set-active-account', payload: account_id });
const connectWebZjsSnap = useCallback(async () => {
try {
await requestSnap();

const latestBlockBigInt = await state.webWallet?.get_latest_block();
const latestBlock = Number(latestBlockBigInt);

const customBirthdayBlock = (await invokeSnap({
method: 'setBirthdayBlock',
params: { latestBlock },
})) as string | null;

const viewingKey = (await invokeSnap({
method: 'getViewingKey',
})) as string;

const creationBlockHeight = Number(
customBirthdayBlock !== null ? customBirthdayBlock : latestBlock,
);

if (state.webWallet === null) {
dispatch({
type: 'set-error',
payload: new Error('Wallet not initialized'),
});
return;
}

const account_id = await state.webWallet.create_account_ufvk(
viewingKey,
creationBlockHeight,
);

dispatch({ type: 'set-active-account', payload: account_id });
if (state.webWallet) {
const summary = await state.webWallet.get_wallet_summary();
console.log('account_balances', summary?.account_balances.length);
}
await syncStateWithWallet();

await flushDbToStore();
},
[dispatch, flushDbToStore, state.webWallet, syncStateWithWallet],
);
} catch (error) {
console.error(error);
dispatch({
type: 'set-error',
payload: new Error(
'Failed to connect to MetaMask Snap and create account',
),
});
throw error;
}
}, [
state.webWallet,
invokeSnap,
dispatch,
syncStateWithWallet,
flushDbToStore,
]);

const triggerRescan = useCallback(async () => {
if (!installedSnap) {
return;
}

if (state.loading) {
dispatch({ type: 'set-error', payload: new Error('App not yet loaded') });
return;
Expand Down Expand Up @@ -145,9 +195,9 @@ export function useWebZjsActions(): WebzjsActions {

return {
getAccountData,
addNewAccountFromUfvk,
triggerRescan,
flushDbToStore,
syncStateWithWallet,
connectWebZjsSnap,
};
}
42 changes: 14 additions & 28 deletions packages/web-wallet/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,35 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { ZcashYellowPNG, FormTransferSvg, MetaMaskLogoPNG } from '../assets';
import { useNavigate } from 'react-router-dom';
import { useWebZjsContext } from '../context/WebzjsContext';
import {
useInvokeSnap,
useRequestSnap,
useMetaMask,
useWebZjsActions,
} from '../hooks';

const Home: React.FC = () => {
const [birthdayHeight, setBirthdayHeight] = useState(0);
const navigate = useNavigate();
const { state } = useWebZjsContext();
const { flushDbToStore, addNewAccountFromUfvk } = useWebZjsActions();
const invokeSnap = useInvokeSnap();
const { getAccountData, connectWebZjsSnap } = useWebZjsActions();
const { installedSnap, isFlask } = useMetaMask();
const requestSnap = useRequestSnap();

useEffect(() => {
const fetchBirthday = async () => {
const birthday = await state.webWallet?.get_latest_block();
setBirthdayHeight(Number(birthday) || 0);
};
fetchBirthday();
}, [state]);

const handleRequestSnapAndGetViewingKey: React.MouseEventHandler<
const handleConnectButton: React.MouseEventHandler<
HTMLButtonElement
> = async (e) => {
e.preventDefault();
await requestSnap();

const viewKey = (await invokeSnap({ method: 'getViewingKey' })) as string;
console.log(viewKey);

await addNewAccountFromUfvk(viewKey, birthdayHeight);
setBirthdayHeight(0);

await flushDbToStore();
await connectWebZjsSnap();
};

useEffect(() => {
if (installedSnap) navigate('/dashboard/account-summary');
}, [installedSnap, navigate]);
if (installedSnap) {
const homeReload = async () => {
const accountData = await getAccountData();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we get Account data only on the Receive page?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced unifiedAddress instead installedSnap as a flag that allows /dashboard/account-summary.

Maybe checking wallet summary is better for this case


if (accountData?.unifiedAddress) navigate('/dashboard/account-summary')
}
homeReload();
};
}, [installedSnap, navigate, getAccountData, state.activeAccount]);

return (
<div className="home-page flex items-start md:items-center justify-center px-4">
Expand All @@ -63,7 +49,7 @@ const Home: React.FC = () => {
</p>
<button
disabled={!isFlask}
onClick={handleRequestSnapAndGetViewingKey}
onClick={handleConnectButton}
className="flex items-center bg-button-black-gradient hover:bg-button-black-gradient-hover text-white px-6 py-3 rounded-[2rem] cursor-pointer"
>
<span>Connect MetaMask Snap</span>
Expand Down
Loading