From 74c4992f83b18bd6cf275e1fb9c34e5327873989 Mon Sep 17 00:00:00 2001 From: orhoj Date: Thu, 29 Jun 2023 16:01:25 +0200 Subject: [PATCH 01/14] Add Topbar component and use it --- .../src/assets/svg/back-icon.svg | 15 +++++ .../browser-wallet/src/assets/svg/more.svg | 4 ++ .../VerifiableCredential.scss | 11 +++- .../VerifiableCredentialList.tsx | 51 +++++++++------ .../src/popup/shared/Topbar/Topbar.scss | 33 ++++++++++ .../popup/shared/Topbar/Topbar.stories.tsx | 44 +++++++++++++ .../src/popup/shared/Topbar/Topbar.tsx | 63 +++++++++++++++++++ .../src/popup/shared/Topbar/index.ts | 1 + .../src/popup/styles/_components.scss | 1 + .../src/popup/styles/config/_typography.scss | 8 +++ 10 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 packages/browser-wallet/src/assets/svg/back-icon.svg create mode 100644 packages/browser-wallet/src/assets/svg/more.svg create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx create mode 100644 packages/browser-wallet/src/popup/shared/Topbar/index.ts diff --git a/packages/browser-wallet/src/assets/svg/back-icon.svg b/packages/browser-wallet/src/assets/svg/back-icon.svg new file mode 100644 index 00000000..c8fa3bd8 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/back-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/browser-wallet/src/assets/svg/more.svg b/packages/browser-wallet/src/assets/svg/more.svg new file mode 100644 index 00000000..0abc2086 --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/more.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss index 9cef5704..47e42223 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredential.scss @@ -1,4 +1,6 @@ .verifiable-credential-list { + height: calc(100% - 56px); + background-color: $color-bg; overflow-y: auto; } @@ -6,7 +8,14 @@ color: $color-white; border-radius: rem(16px); background: #148f9d; - margin: rem(16px); + margin-left: rem(16px); + margin-right: rem(16px); + margin-bottom: rem(16px); + + &:not(:first-child) { + margin-top: rem(16px); + } + box-shadow: rgb(99 99 99 / 20%) rem(0) rem(2px) rem(8px) rem(0); position: relative; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 6769b691..157167fe 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -5,6 +5,7 @@ import { } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; +import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; @@ -47,28 +48,40 @@ export default function VerifiableCredentialList() { if (selected) { return ( - useCredentialStatus(cred)} - /> + <> + setSelected(undefined) }} + menuButton={{ type: ButtonTypes.More, onClick: () => {} }} + /> +
+ useCredentialStatus(cred)} + /> +
+ ); } return ( -
- {verifiableCredentials.map((credential, index) => { - return ( - setSelected(credential)} - useCredentialStatus={(cred) => useCredentialStatus(cred)} - /> - ); - })} -
+ <> + +
+ {verifiableCredentials.map((credential, index) => { + return ( + setSelected(credential)} + useCredentialStatus={(cred) => useCredentialStatus(cred)} + /> + ); + })} +
+ ); } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss new file mode 100644 index 00000000..0bd3da8b --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss @@ -0,0 +1,33 @@ +.topbar { + height: 56px; + background-color: $color-bg; + color: $color-cta; + padding-left: 16px; + padding-right: 16px; + display: flex; + align-items: center; + justify-content: center; + + &__title { + text-align: center; + width: 100%; + } + + &__icon-container { + height: 24px; + width: 24px; + flex-shrink: 0; + + &__icon { + display: flex; + align-items: center; + height: 24px; + width: 24px; + stroke-width: 0; + + path { + fill: $color-cta; + } + } + } +} diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx new file mode 100644 index 00000000..0df3dcaf --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx @@ -0,0 +1,44 @@ +/* eslint-disable react/function-component-definition */ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; +import Topbar, { ButtonTypes } from './Topbar'; + +export default { + title: 'Shared/Topbar', + component: Topbar, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + return ( + +
+ +
+
+ ); +}; + +export const WithBackButton = Template.bind({}); +WithBackButton.args = { + title: 'Page Navigation Title', +}; + +export const WithoutBackButton = Template.bind({}); +WithoutBackButton.args = { + title: 'Page Navigation Title', + backButton: { show: false }, +}; + +export const WithMoreMenuButton = Template.bind({}); +WithMoreMenuButton.args = { + title: 'Page Navigation Title', + backButton: { show: false }, + menuButton: { type: ButtonTypes.More, onClick: () => {} }, +}; + +export const WithBackAndMoreMenuButton = Template.bind({}); +WithBackAndMoreMenuButton.args = { + title: 'Page Navigation Title', + menuButton: { type: ButtonTypes.More, onClick: () => {} }, +}; diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx new file mode 100644 index 00000000..30ec1789 --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import BackIcon from '@assets/svg/back-icon.svg'; +import MoreIcon from '@assets/svg/more.svg'; +import { useNavigate } from 'react-router-dom'; +import Button from '../Button'; + +export enum ButtonTypes { + More, +} + +interface NoBackButton { + show: false; +} + +interface ShowBackButton { + show: true; + onClick: () => void; +} + +type BackButton = ShowBackButton | NoBackButton; + +interface MenuButton { + type: ButtonTypes; + onClick: () => void; +} + +interface TopbarProps { + title: string; + backButton?: BackButton; + menuButton?: MenuButton; +} + +export default function Topbar({ + title, + backButton = { + show: true, + onClick: () => { + const nav = useNavigate(); + return nav(-1); + }, + }, + menuButton, +}: TopbarProps) { + return ( +
+
+ {backButton.show && ( + + )} +
+
{title}
+
+ {menuButton && ( + + )} +
+
+ ); +} diff --git a/packages/browser-wallet/src/popup/shared/Topbar/index.ts b/packages/browser-wallet/src/popup/shared/Topbar/index.ts new file mode 100644 index 00000000..91156873 --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/Topbar/index.ts @@ -0,0 +1 @@ +export { default } from './Topbar'; diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index d3845a5d..948dbc1d 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -21,6 +21,7 @@ @import '../shared/TokenDetails/TokenDetails'; @import '../shared/ContractTokenLine/ContractTokenLine'; @import '../shared/DisabledAmountInput/DisabledAmountInput'; +@import '../shared/Topbar/Topbar'; // Pages @import '../pages/Account'; diff --git a/packages/browser-wallet/src/popup/styles/config/_typography.scss b/packages/browser-wallet/src/popup/styles/config/_typography.scss index 132c6e82..e1ee33ac 100644 --- a/packages/browser-wallet/src/popup/styles/config/_typography.scss +++ b/packages/browser-wallet/src/popup/styles/config/_typography.scss @@ -1,6 +1,7 @@ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@200;300;400;500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@600&display=swap'); $font-family-roboto: 'Roboto', 'Inter', sans-serif; $font-family-mono: 'Roboto Mono', 'Inter', sans-serif; @@ -8,3 +9,10 @@ $font-weight-extra-light: 200; $font-weight-light: 300; $font-weight-regular: 400; $font-weight-bold: 500; + +.display6 { + font-family: 'Work Sans', sans-serif; + font-weight: 600; + font-size: 14px; + line-height: 17px; +} From 4dc891b25bc688c28730074ff7dd9a46bc9f9e34 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 30 Jun 2023 09:03:19 +0200 Subject: [PATCH 02/14] Add popup menu component --- .../browser-wallet/src/assets/svg/archive.svg | 15 +++++++ .../browser-wallet/src/assets/svg/revoke.svg | 3 ++ .../src/popup/shared/PopupMenu/PopupMenu.scss | 41 +++++++++++++++++++ .../shared/PopupMenu/PopupMenu.stories.tsx | 35 ++++++++++++++++ .../src/popup/shared/PopupMenu/PopupMenu.tsx | 27 ++++++++++++ .../src/popup/shared/PopupMenu/index.ts | 1 + .../src/popup/styles/_components.scss | 1 + .../src/popup/styles/config/_typography.scss | 7 ++++ 8 files changed, 130 insertions(+) create mode 100644 packages/browser-wallet/src/assets/svg/archive.svg create mode 100644 packages/browser-wallet/src/assets/svg/revoke.svg create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx create mode 100644 packages/browser-wallet/src/popup/shared/PopupMenu/index.ts diff --git a/packages/browser-wallet/src/assets/svg/archive.svg b/packages/browser-wallet/src/assets/svg/archive.svg new file mode 100644 index 00000000..f9dbe4ce --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/archive.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/browser-wallet/src/assets/svg/revoke.svg b/packages/browser-wallet/src/assets/svg/revoke.svg new file mode 100644 index 00000000..15142a8f --- /dev/null +++ b/packages/browser-wallet/src/assets/svg/revoke.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss new file mode 100644 index 00000000..c1926c9b --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -0,0 +1,41 @@ +.popup-menu { + width: 200px; + box-shadow: 0 0 20px rgb(0 0 0 / 15%); + border-radius: 16px; + display: flex; + flex-direction: column; + + &__item { + &:hover { + background-color: lightgray; + } + + &:first-child { + border-top-left-radius: 16px; + border-top-right-radius: 16px; + } + + &:last-child { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } + + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + height: 44px; + + &:not(:last-child) { + border-bottom: 1px solid lightgray; + } + + &__icon { + height: 20px; + width: 20px; + display: flex; + align-items: center; + justify-content: center; + } + } +} diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx new file mode 100644 index 00000000..864aa61f --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.stories.tsx @@ -0,0 +1,35 @@ +/* eslint-disable react/function-component-definition */ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; +import RevokeIcon from '@assets/svg/revoke.svg'; +import ArchiveIcon from '@assets/svg/archive.svg'; +import PopupMenu from './PopupMenu'; + +export default { + title: 'Shared/PopupMenu', + component: PopupMenu, +} as ComponentMeta; + +const Template: ComponentStory = (args) => { + return ( + +
+ +
+
+ ); +}; + +export const WithSingleItem = Template.bind({}); +WithSingleItem.args = { + items: [{ title: 'Revoke', icon: }], +}; + +export const WithTwoItems = Template.bind({}); +WithTwoItems.args = { + items: [ + { title: 'Revoke', icon: }, + { title: 'Archive', icon: }, + ], +}; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx new file mode 100644 index 00000000..b4136bdc --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Button from '../Button/Button'; + +interface PopupMenuItem { + title: string; + icon: JSX.Element; + onClick?: () => void; +} + +interface PopupMenuProps { + items: PopupMenuItem[]; +} + +export default function PopupMenu({ items }: PopupMenuProps) { + return ( +
+ {items.map((item) => { + return ( + + ); + })} +
+ ); +} diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts b/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts new file mode 100644 index 00000000..be13378e --- /dev/null +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/index.ts @@ -0,0 +1 @@ +export { default } from './PopupMenu'; diff --git a/packages/browser-wallet/src/popup/styles/_components.scss b/packages/browser-wallet/src/popup/styles/_components.scss index 948dbc1d..8c580ac3 100644 --- a/packages/browser-wallet/src/popup/styles/_components.scss +++ b/packages/browser-wallet/src/popup/styles/_components.scss @@ -22,6 +22,7 @@ @import '../shared/ContractTokenLine/ContractTokenLine'; @import '../shared/DisabledAmountInput/DisabledAmountInput'; @import '../shared/Topbar/Topbar'; +@import '../shared/PopupMenu/PopupMenu'; // Pages @import '../pages/Account'; diff --git a/packages/browser-wallet/src/popup/styles/config/_typography.scss b/packages/browser-wallet/src/popup/styles/config/_typography.scss index e1ee33ac..ac33a567 100644 --- a/packages/browser-wallet/src/popup/styles/config/_typography.scss +++ b/packages/browser-wallet/src/popup/styles/config/_typography.scss @@ -16,3 +16,10 @@ $font-weight-bold: 500; font-size: 14px; line-height: 17px; } + +.heading6 { + font-family: $font-family-roboto; + font-weight: 500; + font-size: 14px; + line-height: 18px; +} From 46c9d1c9ced2cf973f402ec2d926489d2426e945 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 30 Jun 2023 10:25:24 +0200 Subject: [PATCH 03/14] Add revoke button to credential details view --- .../VerifiableCredentialList.tsx | 16 +++++++---- .../pages/VerifiableCredential/i18n/da.ts | 13 +++++++++ .../pages/VerifiableCredential/i18n/en.ts | 11 ++++++++ .../src/popup/shared/PopupMenu/PopupMenu.scss | 1 + .../src/popup/shared/PopupMenu/PopupMenu.tsx | 28 +++++++++++-------- .../src/popup/shared/Topbar/Topbar.scss | 12 ++++++++ .../popup/shared/Topbar/Topbar.stories.tsx | 4 +-- .../src/popup/shared/Topbar/Topbar.tsx | 19 +++++++++---- .../src/popup/shell/i18n/locales/da.ts | 2 ++ .../src/popup/shell/i18n/locales/en.ts | 2 ++ 10 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 157167fe..9a60af8a 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -6,8 +6,10 @@ import { import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; +import { useTranslation } from 'react-i18next'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; +import RevokeIcon from '../../../assets/svg/revoke.svg'; /** * Component to display when there are no verifiable credentials in the wallet. @@ -29,6 +31,7 @@ export default function VerifiableCredentialList() { const verifiableCredentials = useAtomValue(storedVerifiableCredentialsAtom); const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const [selected, setSelected] = useState(); + const { t } = useTranslation('verifiableCredential'); if (schemas.loading) { return null; @@ -47,12 +50,14 @@ export default function VerifiableCredentialList() { } if (selected) { + const menuButton = { type: ButtonTypes.More, items: [{ title: t('menu.revoke'), icon: }] }; + return ( <> setSelected(undefined) }} - menuButton={{ type: ButtonTypes.More, onClick: () => {} }} + menuButton={menuButton} />
- +
- {verifiableCredentials.map((credential, index) => { + {verifiableCredentials.map((credential) => { return ( setSelected(credential)} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts new file mode 100644 index 00000000..df90dc40 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/da.ts @@ -0,0 +1,13 @@ +import type en from './en'; + +const t: typeof en = { + topbar: { + details: 'Kortdetaljer', + list: 'Web3 ID Kort', + }, + menu: { + revoke: 'Ophæv', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts new file mode 100644 index 00000000..3931b04d --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/i18n/en.ts @@ -0,0 +1,11 @@ +const t = { + topbar: { + details: 'Credential Details', + list: 'Web3 ID Credentials', + }, + menu: { + revoke: 'Revoke', + }, +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss index c1926c9b..4ff9c03c 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -4,6 +4,7 @@ border-radius: 16px; display: flex; flex-direction: column; + background-color: $color-white; &__item { &:hover { diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index b4136bdc..90c67d67 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import { DetectClickOutside } from 'wallet-common-helpers'; import Button from '../Button/Button'; -interface PopupMenuItem { +export interface PopupMenuItem { title: string; icon: JSX.Element; onClick?: () => void; @@ -9,19 +10,22 @@ interface PopupMenuItem { interface PopupMenuProps { items: PopupMenuItem[]; + onClickOutside: () => void; } -export default function PopupMenu({ items }: PopupMenuProps) { +export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) { return ( -
- {items.map((item) => { - return ( - - ); - })} -
+ +
+ {items.map((item) => { + return ( + + ); + })} +
+
); } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss index 0bd3da8b..8ad3eefc 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.scss @@ -1,4 +1,5 @@ .topbar { + position: relative; height: 56px; background-color: $color-bg; color: $color-cta; @@ -30,4 +31,15 @@ } } } + + &__popup-menu { + display: none; + position: absolute; + right: 10px; + + &__show { + z-index: 1; + display: block; + } + } } diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx index 0df3dcaf..4afbd4e9 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.stories.tsx @@ -34,11 +34,11 @@ export const WithMoreMenuButton = Template.bind({}); WithMoreMenuButton.args = { title: 'Page Navigation Title', backButton: { show: false }, - menuButton: { type: ButtonTypes.More, onClick: () => {} }, + menuButton: { type: ButtonTypes.More, items: [{ title: 'Revoke', icon:
Test
}] }, }; export const WithBackAndMoreMenuButton = Template.bind({}); WithBackAndMoreMenuButton.args = { title: 'Page Navigation Title', - menuButton: { type: ButtonTypes.More, onClick: () => {} }, + menuButton: { type: ButtonTypes.More, items: [{ title: 'Revoke', icon:
Test
}] }, }; diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index 30ec1789..fe636a96 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import BackIcon from '@assets/svg/back-icon.svg'; import MoreIcon from '@assets/svg/more.svg'; import { useNavigate } from 'react-router-dom'; +import clsx from 'clsx'; import Button from '../Button'; +import PopupMenu, { PopupMenuItem } from '../PopupMenu/PopupMenu'; export enum ButtonTypes { More, @@ -19,11 +21,13 @@ interface ShowBackButton { type BackButton = ShowBackButton | NoBackButton; -interface MenuButton { - type: ButtonTypes; - onClick: () => void; +interface MoreMenuButton { + type: ButtonTypes.More; + items: PopupMenuItem[]; } +type MenuButton = MoreMenuButton; + interface TopbarProps { title: string; backButton?: BackButton; @@ -41,6 +45,8 @@ export default function Topbar({ }, menuButton, }: TopbarProps) { + const [showPopupMenu, setShowPopupMenu] = useState(false); + return (
@@ -53,8 +59,11 @@ export default function Topbar({
{title}
{menuButton && ( - )}
diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts index 1f9eba3a..a1fc8d17 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/da.ts @@ -20,6 +20,7 @@ import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/da'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/da'; import allowlist from '@popup/pages/Allowlist/i18n/da'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/da'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/da'; import type en from './en'; @@ -46,6 +47,7 @@ const t: typeof en = { idProofRequest, allowlist, connectAccountsRequest, + verifiableCredential, }; export default t; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index ae78022e..107a2621 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -20,6 +20,7 @@ import termsAndConditions from '@popup/pages/TermsAndConditions/i18n/en'; import idProofRequest from '@popup/pages/IdProofRequest/i18n/en'; import allowlist from '@popup/pages/Allowlist/i18n/en'; import connectAccountsRequest from '@popup/pages/ConnectAccountsRequest/i18n/en'; +import verifiableCredential from '@popup/pages/VerifiableCredential/i18n/en'; const t = { shared, @@ -44,6 +45,7 @@ const t = { idProofRequest, allowlist, connectAccountsRequest, + verifiableCredential, }; export default t; From d45ba7091e76dfa82ae6f2f69a4b405647402a8f Mon Sep 17 00:00:00 2001 From: orhoj Date: Mon, 3 Jul 2023 10:16:43 +0200 Subject: [PATCH 04/14] Initial revoke UI and methods --- packages/browser-wallet/package.json | 1 + .../VerifiableCredentialDetails.tsx | 128 ++++++++++++++++++ .../VerifiableCredentialList.tsx | 23 +--- .../src/popup/shared/Topbar/Topbar.tsx | 2 +- .../utils/verifiable-credential-helpers.ts | 54 ++++++++ yarn.lock | 8 ++ 6 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e7c2c7ea..3db7aa34 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -20,6 +20,7 @@ "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", "@concordium/web-sdk": "^4.0.1", + "@noble/ed25519": "^2.0.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx new file mode 100644 index 00000000..5437fd67 --- /dev/null +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Buffer } from 'buffer/'; +import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; +import { useAtomValue } from 'jotai'; +import { VerifiableCredential } from '@shared/storage/types'; +import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar'; +import { useTranslation } from 'react-i18next'; +import { AccountTransactionType, CcdAmount, ConcordiumGRPCClient, UpdateContractPayload } from '@concordium/web-sdk'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + RevocationDataHolder, + RevokeCredentialHolderParam, + SigningData, + getCredentialHolderId, + getCredentialRegistryContractAddress, + serializeRevokeCredentialHolderParam, + sign, +} from '@shared/utils/verifiable-credential-helpers'; +import { fetchContractName } from '@shared/utils/token-helpers'; +import { grpcClientAtom } from '@popup/store/settings'; +import { absoluteRoutes } from '@popup/constants/routes'; +import { selectedAccountAtom } from '@popup/store/account'; +import { usePrivateKey } from '@popup/shared/utils/account-helpers'; +import { accountRoutes } from '../Account/routes'; +import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer'; +import RevokeIcon from '../../../assets/svg/revoke.svg'; +import { useCredentialStatus } from './VerifiableCredentialHooks'; +import { VerifiableCredentialCard } from './VerifiableCredentialCard'; + +const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; + +async function buildRevokeTransaction( + client: ConcordiumGRPCClient, + credential: VerifiableCredential, + privateKey: string +): Promise { + const address = getCredentialRegistryContractAddress(credential.id); + const contractName = await fetchContractName(client, address.index, address.subindex); + + // TODO Get the correct nonce. + + const signingData: SigningData = { + contractAddress: address, + entryPoint: 'revokeCredentialHolder', + nonce: BigInt(1), + timestamp: BigInt(Date.now() + 60000), + }; + + const data: RevocationDataHolder = { + credentialId: getCredentialHolderId(credential.id), + signingData, + }; + + const signature = await sign(Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), privateKey); + const parameter: RevokeCredentialHolderParam = { + signature, + data, + }; + + // Get better NRG estimate. How to do that? + return { + address, + amount: new CcdAmount(0n), + receiveName: `${contractName}.revokeCredentialHolder`, + maxContractExecutionEnergy: BigInt(5000), + message: serializeRevokeCredentialHolderParam(parameter), + }; +} + +export default function VerifiableCredentialDetails({ + credential, + backButtonOnClick, +}: { + credential: VerifiableCredential; + backButtonOnClick: () => void; +}) { + const nav = useNavigate(); + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const { t } = useTranslation('verifiableCredential'); + + const client = useAtomValue(grpcClientAtom); + const { pathname } = useLocation(); + + const selectedAccount = useAtomValue(selectedAccountAtom); + const key = usePrivateKey(selectedAccount); + + const goToConfirm = () => { + // TODO Fix this... Basically we need to wait for these values before displaying the card. + if (!selectedAccount || !key) { + return; + } + + buildRevokeTransaction(client, credential, key).then((payload) => { + const confirmTransferState: ConfirmGenericTransferState = { + payload, + type: AccountTransactionType.Update, + }; + + // Override current router entry with stateful version + nav(pathname, { replace: true, state: true }); + nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { + state: confirmTransferState, + }); + }); + }; + + const menuButton: MenuButton = { + type: ButtonTypes.More, + items: [{ title: t('menu.revoke'), icon: , onClick: () => goToConfirm() }], + }; + + return ( + <> + +
+ useCredentialStatus(cred)} + /> +
+ + ); +} diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx index 9a60af8a..98ed9dd8 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialList.tsx @@ -5,11 +5,11 @@ import { } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; -import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar'; +import Topbar from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; import { useCredentialStatus } from './VerifiableCredentialHooks'; -import RevokeIcon from '../../../assets/svg/revoke.svg'; +import VerifiableCredentialDetails from './VerifiableCredentialDetails'; /** * Component to display when there are no verifiable credentials in the wallet. @@ -50,24 +50,7 @@ export default function VerifiableCredentialList() { } if (selected) { - const menuButton = { type: ButtonTypes.More, items: [{ title: t('menu.revoke'), icon: }] }; - - return ( - <> - setSelected(undefined) }} - menuButton={menuButton} - /> -
- useCredentialStatus(cred)} - /> -
- - ); + return setSelected(undefined)} />; } return ( diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index fe636a96..ca8e9aa8 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -26,7 +26,7 @@ interface MoreMenuButton { items: PopupMenuItem[]; } -type MenuButton = MoreMenuButton; +export type MenuButton = MoreMenuButton; interface TopbarProps { title: string; diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 956a5942..b54e5398 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,6 +1,60 @@ import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; +import * as ed from '@noble/ed25519'; + +export async function sign(digest: Buffer, privateKey: string) { + return Buffer.from(await ed.signAsync(digest, privateKey)).toString('hex'); +} + +export interface SigningData { + contractAddress: ContractAddress; + entryPoint: string; + nonce: bigint; + timestamp: bigint; +} + +export interface RevocationDataHolder { + /** The public key identifying the credential holder */ + credentialId: string; + /** Metadata of the signature */ + signingData: SigningData; +} + +export interface RevokeCredentialHolderParam { + /** Ed25519 signature on the revocation message as a hex string */ + signature: string; + /** Revocation data */ + data: RevocationDataHolder; +} + +export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { + let buffer = Buffer.from(parameter.signature, 'hex'); + buffer = Buffer.concat([buffer, Buffer.from(parameter.data.credentialId, 'hex')]); + + // TODO Fix BigInt issue here. + const addressBuffer = Buffer.alloc(16); + addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.index, 0); + addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.subindex, 8); + + const serializedEntryPointName = Buffer.from(parameter.data.signingData.entryPoint, 'utf-8'); + let entryPointSerialization = Buffer.alloc(2); + entryPointSerialization.writeUInt16LE(serializedEntryPointName.length, 0); + entryPointSerialization = Buffer.concat([entryPointSerialization, serializedEntryPointName]); + + const finalBuffer = Buffer.alloc(16); + finalBuffer.writeBigUInt64LE(parameter.data.signingData.nonce, 0); + finalBuffer.writeBigUInt64LE(parameter.data.signingData.timestamp, 8); + + return Buffer.concat([buffer, addressBuffer, entryPointSerialization, finalBuffer, Buffer.of(0)]); +} + +/** + * contractAddress: ContractAddress; + entryPoint: string; + nonce: BigInt; + timestamp: BigInt; + */ /** * Extracts the credential holder id from a verifiable credential id (did). diff --git a/yarn.lock b/yarn.lock index 07a81131..1441dc1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,6 +1961,7 @@ __metadata: "@concordium/web-sdk": ^4.0.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 + "@noble/ed25519": ^2.0.0 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 "@storybook/addon-actions": ^6.5.7 @@ -3057,6 +3058,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^2.0.0": + version: 2.0.0 + resolution: "@noble/ed25519@npm:2.0.0" + checksum: 4404deb3ca4f7a07863a362d697dc624ff0e82083ca4e41f976f76ec6fe879363b4722529389aa1ae280fe62558d9f229c8403b0a077add8b5f1ec1290d6e2d7 + languageName: node + linkType: hard + "@noble/hashes@npm:~1.1.1": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" From 746312a2f0532a83da4321f667ba7e4af3bbfd21 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 10:36:55 +0200 Subject: [PATCH 05/14] Revocation improvements and testing --- packages/browser-wallet/package.json | 2 +- .../VerifiableCredentialDetails.tsx | 142 ++++----- .../VerifiableCredentialHooks.tsx | 24 +- .../src/popup/shared/PopupMenu/PopupMenu.scss | 4 + .../src/popup/shared/PopupMenu/PopupMenu.tsx | 7 +- .../src/popup/shared/Topbar/Topbar.tsx | 8 +- .../utils/verifiable-credential-helpers.ts | 292 +++++++++++++++--- .../verifiable-credential-helpers.test.ts | 50 +++ yarn.lock | 16 +- 9 files changed, 412 insertions(+), 133 deletions(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 3db7aa34..52521c4a 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -20,7 +20,7 @@ "@concordium/browser-wallet-api": "workspace:^", "@concordium/browser-wallet-api-helpers": "workspace:^", "@concordium/web-sdk": "^4.0.1", - "@noble/ed25519": "^2.0.0", + "@noble/ed25519": "^1.7.0", "@protobuf-ts/runtime-rpc": "^2.8.2", "@scure/bip39": "^1.1.0", "axios": "^0.27.2", diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 5437fd67..dbe1990f 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -1,70 +1,33 @@ -import React from 'react'; -import { Buffer } from 'buffer/'; +import React, { useCallback, useMemo } from 'react'; import { storedVerifiableCredentialSchemasAtom } from '@popup/store/verifiable-credential'; import { useAtomValue } from 'jotai'; import { VerifiableCredential } from '@shared/storage/types'; import Topbar, { ButtonTypes, MenuButton } from '@popup/shared/Topbar/Topbar'; import { useTranslation } from 'react-i18next'; -import { AccountTransactionType, CcdAmount, ConcordiumGRPCClient, UpdateContractPayload } from '@concordium/web-sdk'; +import { AccountTransactionType } from '@concordium/web-sdk'; import { useLocation, useNavigate } from 'react-router-dom'; +import { grpcClientAtom } from '@popup/store/settings'; +import { absoluteRoutes } from '@popup/constants/routes'; +import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { - RevocationDataHolder, - RevokeCredentialHolderParam, - SigningData, + buildRevokeTransaction, getCredentialHolderId, getCredentialRegistryContractAddress, - serializeRevokeCredentialHolderParam, - sign, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; -import { grpcClientAtom } from '@popup/store/settings'; -import { absoluteRoutes } from '@popup/constants/routes'; -import { selectedAccountAtom } from '@popup/store/account'; -import { usePrivateKey } from '@popup/shared/utils/account-helpers'; import { accountRoutes } from '../Account/routes'; import { ConfirmGenericTransferState } from '../Account/ConfirmGenericTransfer'; import RevokeIcon from '../../../assets/svg/revoke.svg'; -import { useCredentialStatus } from './VerifiableCredentialHooks'; +import { useCredentialEntry, useCredentialStatus } from './VerifiableCredentialHooks'; import { VerifiableCredentialCard } from './VerifiableCredentialCard'; -const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; - -async function buildRevokeTransaction( - client: ConcordiumGRPCClient, - credential: VerifiableCredential, - privateKey: string -): Promise { - const address = getCredentialRegistryContractAddress(credential.id); - const contractName = await fetchContractName(client, address.index, address.subindex); - - // TODO Get the correct nonce. - - const signingData: SigningData = { - contractAddress: address, - entryPoint: 'revokeCredentialHolder', - nonce: BigInt(1), - timestamp: BigInt(Date.now() + 60000), - }; - - const data: RevocationDataHolder = { - credentialId: getCredentialHolderId(credential.id), - signingData, - }; - - const signature = await sign(Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), privateKey); - const parameter: RevokeCredentialHolderParam = { - signature, - data, - }; - - // Get better NRG estimate. How to do that? - return { - address, - amount: new CcdAmount(0n), - receiveName: `${contractName}.revokeCredentialHolder`, - maxContractExecutionEnergy: BigInt(5000), - message: serializeRevokeCredentialHolderParam(parameter), - }; +/** + * Calculates the next revocation nonce based on the current revocation nonce. + * @param nonce the current nonce returned in the credential entry + * @returns the next nonce to use for a holder revocation update + */ +function getNextRevocationNonce(nonce: bigint) { + return nonce + 1n; } export default function VerifiableCredentialDetails({ @@ -75,39 +38,68 @@ export default function VerifiableCredentialDetails({ backButtonOnClick: () => void; }) { const nav = useNavigate(); - const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); + const { pathname } = useLocation(); const { t } = useTranslation('verifiableCredential'); - + const schemas = useAtomValue(storedVerifiableCredentialSchemasAtom); const client = useAtomValue(grpcClientAtom); - const { pathname } = useLocation(); - - const selectedAccount = useAtomValue(selectedAccountAtom); - const key = usePrivateKey(selectedAccount); + const hdWallet = useHdWallet(); + const credentialEntry = useCredentialEntry(credential); - const goToConfirm = () => { - // TODO Fix this... Basically we need to wait for these values before displaying the card. - if (!selectedAccount || !key) { + const goToConfirmPage = useCallback(async () => { + if (credentialEntry === undefined || hdWallet === undefined) { return; } - buildRevokeTransaction(client, credential, key).then((payload) => { - const confirmTransferState: ConfirmGenericTransferState = { - payload, - type: AccountTransactionType.Update, - }; + const contractAddress = getCredentialRegistryContractAddress(credential.id); + const credentialId = getCredentialHolderId(credential.id); + const contractName = await fetchContractName(client, contractAddress.index, contractAddress.subindex); + if (contractName === undefined) { + throw new Error(`Unable to find contract name for address: ${contractAddress}`); + } + const revocationNonce = getNextRevocationNonce(credentialEntry.revocationNonce); + const signingKey = hdWallet.getVerifiableCredentialSigningKey(0).toString('hex'); + const payload = await buildRevokeTransaction( + contractAddress, + contractName, + credentialId, + revocationNonce, + signingKey + ); + + const confirmTransferState: ConfirmGenericTransferState = { + payload, + type: AccountTransactionType.Update, + }; - // Override current router entry with stateful version - nav(pathname, { replace: true, state: true }); - nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { - state: confirmTransferState, - }); + // Override current router entry with stateful version + nav(pathname, { replace: true, state: true }); + nav(`${absoluteRoutes.home.account.path}/${accountRoutes.confirmTransfer}`, { + state: confirmTransferState, }); - }; + }, [client, credential, hdWallet, credentialEntry, nav, pathname]); + + const menuButton: MenuButton | undefined = useMemo(() => { + if (credentialEntry === undefined) { + return undefined; + } + + return { + type: ButtonTypes.More, + items: [ + { + title: t('menu.revoke'), + icon: , + onClick: credentialEntry.holderRevocable ? () => goToConfirmPage() : undefined, + }, + ], + }; + }, [credentialEntry, goToConfirmPage]); - const menuButton: MenuButton = { - type: ButtonTypes.More, - items: [{ title: t('menu.revoke'), icon: , onClick: () => goToConfirm() }], - }; + // Wait for the credential entry to be loaded from the chain, and for the HdWallet + // to be loaded to be ready to derive keys. + if (credentialEntry === undefined || hdWallet === undefined) { + return null; + } return ( <> diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 5a56e0a8..3d207c30 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,15 +1,37 @@ import { grpcClientAtom } from '@popup/store/settings'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { + CredentialEntry, getCredentialHolderId, getCredentialRegistryContractAddress, + getVerifiableCredentialEntry, getVerifiableCredentialStatus, } from '@shared/utils/verifiable-credential-helpers'; import { useAtomValue } from 'jotai'; import { useEffect, useState } from 'react'; /** - * Retrieve the on-chain credential status for a verifiable credential in a registry contract. + * Retrieve the on-chain credential entry for a verifiable credential in a CIS-4 credential registry contract. + * @param credential the verifiable credential to retrieve the credential entry for + * @returns the credential entry for the given credential, undefined if one is not found yet + */ +export function useCredentialEntry(credential: VerifiableCredential) { + const [credentialEntry, setCredentialEntry] = useState(); + const client = useAtomValue(grpcClientAtom); + + useEffect(() => { + const credentialHolderId = getCredentialHolderId(credential.id); + const registryContractAddress = getCredentialRegistryContractAddress(credential.id); + getVerifiableCredentialEntry(client, registryContractAddress, credentialHolderId).then((entry) => { + setCredentialEntry(entry); + }); + }, [credential.id, client]); + + return credentialEntry; +} + +/** + * Retrieve the on-chain credential status for a verifiable credential in a CIS-4 credential registry contract. * @param credential the verifiable credential to lookup the status for * @returns the status for the given credential */ diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss index 4ff9c03c..097c20b7 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.scss @@ -31,6 +31,10 @@ border-bottom: 1px solid lightgray; } + &--disabled { + opacity: 0.2; + } + &__icon { height: 20px; width: 20px; diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index 90c67d67..d7f158e2 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { DetectClickOutside } from 'wallet-common-helpers'; +import clsx from 'clsx'; import Button from '../Button/Button'; export interface PopupMenuItem { @@ -19,7 +20,11 @@ export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) {
{items.map((item) => { return ( - diff --git a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx index ca8e9aa8..0cf61a64 100644 --- a/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx +++ b/packages/browser-wallet/src/popup/shared/Topbar/Topbar.tsx @@ -59,12 +59,14 @@ export default function Topbar({
{title}
{menuButton && ( -
setShowPopupMenu(false)} />
- + )}
diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index b54e5398..25087906 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -1,10 +1,49 @@ -import { ConcordiumGRPCClient, ContractAddress } from '@concordium/web-sdk'; +import { CcdAmount, ConcordiumGRPCClient, ContractAddress, UpdateContractPayload } from '@concordium/web-sdk'; import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; import * as ed from '@noble/ed25519'; +/** + * Extracts the credential holder id from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the credential holder id + */ +export function getCredentialHolderId(credentialId: string): string { + const splitted = credentialId.split('/'); + const credentialHolderId = splitted[splitted.length - 1]; + + if (credentialHolderId.length !== 64) { + throw new Error(`Invalid credential holder id found from: ${credentialId}`); + } + + return credentialHolderId; +} + +/** + * Extracts the credential registry contract addres from a verifiable credential id (did). + * @param credentialId the did for a credential + * @returns the contract address of the issuing contract of the provided credential id + */ +export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { + const splitted = credentialId.split(':'); + const index = BigInt(splitted[4]); + const subindex = BigInt(splitted[5].split('/')[0]); + return { index, subindex }; +} + export async function sign(digest: Buffer, privateKey: string) { - return Buffer.from(await ed.signAsync(digest, privateKey)).toString('hex'); + return Buffer.from(await ed.sign(digest, privateKey)).toString('hex'); +} + +// TODO This function is a copy from the node-sdk. Remove the duplicate here and export it in the SDK to use instead. +export function encodeWord64LE(value: bigint): Buffer { + if (value > 18446744073709551615n || value < 0n) { + throw new Error(`The input has to be a 64 bit unsigned integer but it was: ${value}`); + } + const arr = new ArrayBuffer(8); + const view = new DataView(arr); + view.setBigUint64(0, value, true); + return Buffer.from(new Uint8Array(arr)); } export interface SigningData { @@ -28,60 +67,93 @@ export interface RevokeCredentialHolderParam { data: RevocationDataHolder; } -export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { - let buffer = Buffer.from(parameter.signature, 'hex'); - buffer = Buffer.concat([buffer, Buffer.from(parameter.data.credentialId, 'hex')]); +/** + * Serializes the revocation data holder. This is the data that is signed, together with a prefix, to authorize + * the revocation of a held credential. + * @param data the revocation data context to serialize + * @returns the serialized revocation data holder + */ +function serializeRevocationDataHolder(data: RevocationDataHolder) { + const credentialId = Buffer.from(data.credentialId, 'hex'); + + const contractIndex = encodeWord64LE(data.signingData.contractAddress.index); + const contractSubindex = encodeWord64LE(data.signingData.contractAddress.subindex); - // TODO Fix BigInt issue here. - const addressBuffer = Buffer.alloc(16); - addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.index, 0); - addressBuffer.writeBigUInt64LE(parameter.data.signingData.contractAddress.subindex, 8); + const entrypointName = Buffer.from(data.signingData.entryPoint, 'utf-8'); + const entrypointLength = Buffer.alloc(2); + entrypointLength.writeUInt16LE(entrypointName.length, 0); - const serializedEntryPointName = Buffer.from(parameter.data.signingData.entryPoint, 'utf-8'); - let entryPointSerialization = Buffer.alloc(2); - entryPointSerialization.writeUInt16LE(serializedEntryPointName.length, 0); - entryPointSerialization = Buffer.concat([entryPointSerialization, serializedEntryPointName]); + const nonce = encodeWord64LE(data.signingData.nonce); + const timestamp = encodeWord64LE(data.signingData.timestamp); - const finalBuffer = Buffer.alloc(16); - finalBuffer.writeBigUInt64LE(parameter.data.signingData.nonce, 0); - finalBuffer.writeBigUInt64LE(parameter.data.signingData.timestamp, 8); + const optionalReason = Buffer.of(0); - return Buffer.concat([buffer, addressBuffer, entryPointSerialization, finalBuffer, Buffer.of(0)]); + return Buffer.concat([ + credentialId, + contractIndex, + contractSubindex, + entrypointLength, + entrypointName, + nonce, + timestamp, + optionalReason, + ]); } -/** - * contractAddress: ContractAddress; - entryPoint: string; - nonce: BigInt; - timestamp: BigInt; - */ +export function serializeRevokeCredentialHolderParam(parameter: RevokeCredentialHolderParam) { + const signature = Buffer.from(parameter.signature, 'hex'); + const data = serializeRevocationDataHolder(parameter.data); + return Buffer.concat([signature, data]); +} /** - * Extracts the credential holder id from a verifiable credential id (did). - * @param credentialId the did for a credential - * @returns the credential holder id + * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. + * @param address the address of the contract that the credential to revoke is registered in + * @param contractName the name of the contract at {@link address} + * @param credentialId the id of the credential to revoke + * @param nonce the revocation nonce + * @param signingKey the signing key associated with the credential (the private key to the {@link credentialId}) + * @returns an update contract transaction for revoking the credential with id {@link credentialId} */ -export function getCredentialHolderId(credentialId: string): string { - const splitted = credentialId.split('/'); - const credentialHolderId = splitted[splitted.length - 1]; +export async function buildRevokeTransaction( + address: ContractAddress, + contractName: string, + credentialId: string, + nonce: bigint, + signingKey: string +): Promise { + const signingData: SigningData = { + contractAddress: address, + entryPoint: 'revokeCredentialHolder', + nonce, + timestamp: BigInt(Date.now() + 5 * 60000), + }; - if (credentialHolderId.length !== 64) { - throw new Error(`Invalid credential holder id found from: ${credentialId}`); - } + const data: RevocationDataHolder = { + credentialId, + signingData, + }; - return credentialHolderId; -} + const REVOKE_SIGNATURE_MESSAGE = 'WEB3ID:REVOKE'; + const serializedData = serializeRevocationDataHolder(data); + const signature = await sign( + Buffer.concat([Buffer.from(REVOKE_SIGNATURE_MESSAGE, 'utf-8'), serializedData]), + signingKey + ); -/** - * Extracts the credential registry contract addres from a verifiable credential id (did). - * @param credentialId the did for a credential - * @returns the contract address of the issuing contract of the provided credential id - */ -export function getCredentialRegistryContractAddress(credentialId: string): ContractAddress { - const splitted = credentialId.split(':'); - const index = BigInt(splitted[4]); - const subindex = BigInt(splitted[5].split('/')[0]); - return { index, subindex }; + const parameter: RevokeCredentialHolderParam = { + signature, + data, + }; + + // Get better NRG estimate. How to do that? Invoke the contract and use that. + return { + address, + amount: new CcdAmount(0n), + receiveName: `${contractName}.revokeCredentialHolder`, + maxContractExecutionEnergy: BigInt(50000), + message: serializeRevokeCredentialHolderParam(parameter), + }; } function deserializeCredentialStatus(serializedCredentialStatus: string): VerifiableCredentialStatus { @@ -135,3 +207,135 @@ export async function getVerifiableCredentialStatus( return deserializeCredentialStatus(returnValue); } + +export interface CredentialEntry { + credentialHolderId: string; + holderRevocable: boolean; + validFrom: bigint; + validUntil?: bigint; + credentialType: string; + metadataUrl: string; + metadataChecksum?: string; + schemaUrl: string; + schemaChecksum?: string; + revocationNonce: bigint; +} + +function deserializeUrlChecksumPair(buffer: Buffer, offset: number) { + let localOffset = offset; + const urlLength = buffer.readUInt16LE(localOffset); + localOffset += 2; + + const url = buffer.toString('utf-8', localOffset, localOffset + urlLength); + localOffset += urlLength; + + const containsChecksum = buffer.readUInt8(localOffset); + localOffset += 1; + + let checksum: string | undefined; + if (containsChecksum) { + checksum = buffer.toString('hex', localOffset, localOffset + 32); + localOffset += 32; + } + + return { + url, + checksum, + offset: localOffset, + }; +} + +/** + * Deserializes a CredentialEntry according to the CIS-4 specification. + * @param serializedCredentialEntry a serialized credential entry as a hex string + */ +export function deserializeCredentialEntry(serializedCredentialEntry: string): CredentialEntry { + const buffer = Buffer.from(serializedCredentialEntry, 'hex'); + let offset = 0; + + const credentialHolderId = buffer.toString('hex', offset, offset + 32); + offset += 32; + + const holderRevocable = Boolean(buffer.readUInt8(offset)); + offset += 1; + + // TODO Remove this as the commitments are moving out of the contract. + const commitmentLength = buffer.readUInt16LE(offset); + offset += 2 + commitmentLength; + // TODO Remove until here. + + const validFrom = buffer.readBigUInt64LE(offset) as bigint; + offset += 8; + + const containsValidUntil = Boolean(buffer.readUInt8(offset)); + offset += 1; + + let validUntil: bigint | undefined; + if (containsValidUntil) { + validUntil = buffer.readBigUInt64LE(offset) as bigint; + offset += 8; + } + + const credentialTypeLength = buffer.readUInt8(offset); + offset += 1; + const credentialType = buffer.toString('utf-8', offset, offset + credentialTypeLength); + offset += credentialTypeLength; + + const metadata = deserializeUrlChecksumPair(buffer, offset); + offset = metadata.offset; + + const schema = deserializeUrlChecksumPair(buffer, offset); + offset = schema.offset; + + const revocationNonce = buffer.readBigInt64LE(offset) as bigint; + offset += 8; + + return { + credentialHolderId, + holderRevocable, + validFrom, + validUntil, + credentialType, + metadataUrl: metadata.url, + metadataChecksum: metadata.checksum, + schemaUrl: schema.url, + schemaChecksum: schema.checksum, + revocationNonce, + }; +} + +/** + * Get a Credential Entry from a CIS-4 contract. + * @param client the GRPC client for accessing a node + * @param contractAddress the address of a CIS-4 contract + * @param credentialHolderId the public key for the credential holder of the entry to retrieve + * @throws an error if the invoke contract call fails, or if no return value is available + * @returns the credential entry which contains data about the credential, undefined if the contract instance is not found + */ +export async function getVerifiableCredentialEntry( + client: ConcordiumGRPCClient, + contractAddress: ContractAddress, + credentialHolderId: string +) { + const instanceInfo = await client.getInstanceInfo(contractAddress); + if (instanceInfo === undefined) { + return undefined; + } + + const result = await client.invokeContract({ + contract: contractAddress, + method: `${instanceInfo.name.substring(5)}.credentialEntry`, + parameter: Buffer.from(credentialHolderId, 'hex'), + }); + + if (result.tag !== 'success') { + throw new Error(result.reason.tag); + } + + const { returnValue } = result; + if (returnValue === undefined) { + throw new Error(`Return value is missing from credentialEntry result in CIS-4 contract: ${contractAddress}`); + } + + return deserializeCredentialEntry(returnValue); +} diff --git a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts index fe8a57d3..bc271dd3 100644 --- a/packages/browser-wallet/test/verifiable-credential-helpers.test.ts +++ b/packages/browser-wallet/test/verifiable-credential-helpers.test.ts @@ -1,8 +1,58 @@ import { + CredentialEntry, + RevocationDataHolder, + RevokeCredentialHolderParam, + SigningData, + deserializeCredentialEntry, getCredentialHolderId, getCredentialRegistryContractAddress, + serializeRevokeCredentialHolderParam, } from '../src/shared/utils/verifiable-credential-helpers'; +test('serializing a revoke credential holder parameter', () => { + const signingData: SigningData = { + contractAddress: { index: 4718n, subindex: 0n }, + entryPoint: 'revokeCredentialHolder', + nonce: 0n, + timestamp: 1688542350309n, + }; + + const data: RevocationDataHolder = { + credentialId: '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106', + signingData, + }; + + const revokeCredentialHolderParam: RevokeCredentialHolderParam = { + signature: + 'a70b2b7987a2835726bc6166da1e4d223b9f215962e20726a39ea95afaf9d10d13d0f093761f2f2a4c34d37f081ea501fed8ab74fb565b87822ff0aec6071309', + data, + }; + + expect(serializeRevokeCredentialHolderParam(revokeCredentialHolderParam).toString('hex')).toEqual( + 'a70b2b7987a2835726bc6166da1e4d223b9f215962e20726a39ea95afaf9d10d13d0f093761f2f2a4c34d37f081ea501fed8ab74fb565b87822ff0aec60713092eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d1066e12000000000000000000000000000016007265766f6b6543726564656e7469616c486f6c6465720000000000000000e58bf7248901000000' + ); +}); + +test('deserializing a credential entry', () => { + const serializedCredentialEntry = + '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d10601300099cc213d3bda6677df0663ec8ec54a2c05ccae0e8b99ba6130f6931ee85c3de04f4d42f0e4bb3f541e5592ade117b0fe26f5c17688010000000c4d7943726564656e7469616c0300666f6f001100687474703a2f2f736368656d612e6f7267000000000000000000'; + + const expected: CredentialEntry = { + credentialHolderId: '2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106', + credentialType: 'MyCredential', + holderRevocable: true, + metadataChecksum: undefined, + metadataUrl: 'foo', + revocationNonce: 0n, + schemaChecksum: undefined, + schemaUrl: 'http://schema.org', + validFrom: 1685619602726n, + validUntil: undefined, + }; + + expect(deserializeCredentialEntry(serializedCredentialEntry)).toEqual(expected); +}); + test('credential holder id is extracted from verifiable credential id field', () => { const id = 'did:ccd:mainnet:sci:4718:0/credentialEntry/2eec102b173118dda466411fc7df88093788a34c3e2a4b0a8891f5c671a9d106'; diff --git a/yarn.lock b/yarn.lock index 1441dc1f..ebb16bf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1961,7 +1961,7 @@ __metadata: "@concordium/web-sdk": ^4.0.1 "@craftamap/esbuild-plugin-html": ^0.4.0 "@mdx-js/react": ^1.6.22 - "@noble/ed25519": ^2.0.0 + "@noble/ed25519": ^1.7.0 "@protobuf-ts/runtime-rpc": ^2.8.2 "@scure/bip39": ^1.1.0 "@storybook/addon-actions": ^6.5.7 @@ -3051,6 +3051,13 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^1.7.0": + version: 1.7.3 + resolution: "@noble/ed25519@npm:1.7.3" + checksum: 45169927d51de513e47bbeebff3a603433c4ac7579e1b8c5034c380a0afedbe85e6959be3d69584a7a5ed6828d638f8f28879003b9bb2fb5f22d8aa2d88fd5fe + languageName: node + linkType: hard + "@noble/ed25519@npm:^1.7.1": version: 1.7.1 resolution: "@noble/ed25519@npm:1.7.1" @@ -3058,13 +3065,6 @@ __metadata: languageName: node linkType: hard -"@noble/ed25519@npm:^2.0.0": - version: 2.0.0 - resolution: "@noble/ed25519@npm:2.0.0" - checksum: 4404deb3ca4f7a07863a362d697dc624ff0e82083ca4e41f976f76ec6fe879363b4722529389aa1ae280fe62558d9f229c8403b0a077add8b5f1ec1290d6e2d7 - languageName: node - linkType: hard - "@noble/hashes@npm:~1.1.1": version: 1.1.2 resolution: "@noble/hashes@npm:1.1.2" From 6a7c4b344827762d7be356cb8737abd00e8aadfa Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 11:24:32 +0200 Subject: [PATCH 06/14] Estimate execution cost of revocation --- .../VerifiableCredentialDetails.tsx | 14 ++++- .../utils/verifiable-credential-helpers.ts | 58 +++++++++++++++---- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index dbe1990f..7b1da0fa 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -11,8 +11,10 @@ import { absoluteRoutes } from '@popup/constants/routes'; import { useHdWallet } from '@popup/shared/utils/account-helpers'; import { buildRevokeTransaction, + buildRevokeTransactionParameters, getCredentialHolderId, getCredentialRegistryContractAddress, + getRevokeTransactionExecutionEnergyEstimate, } from '@shared/utils/verifiable-credential-helpers'; import { fetchContractName } from '@shared/utils/token-helpers'; import { accountRoutes } from '../Account/routes'; @@ -58,13 +60,21 @@ export default function VerifiableCredentialDetails({ } const revocationNonce = getNextRevocationNonce(credentialEntry.revocationNonce); const signingKey = hdWallet.getVerifiableCredentialSigningKey(0).toString('hex'); - const payload = await buildRevokeTransaction( + + const parameters = await buildRevokeTransactionParameters( contractAddress, - contractName, credentialId, revocationNonce, signingKey ); + const maxExecutionEnergy = await getRevokeTransactionExecutionEnergyEstimate(client, contractName, parameters); + const payload = await buildRevokeTransaction( + contractAddress, + contractName, + credentialId, + maxExecutionEnergy, + parameters + ); const confirmTransferState: ConfirmGenericTransferState = { payload, diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index 25087906..e115f0c2 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -107,21 +107,19 @@ export function serializeRevokeCredentialHolderParam(parameter: RevokeCredential } /** - * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. - * @param address the address of the contract that the credential to revoke is registered in - * @param contractName the name of the contract at {@link address} - * @param credentialId the id of the credential to revoke + * Builds the parameters used for a holder revocation transaction to revoke a given credential in a CIS-4 contract. + * @param address the address of the contract that hte credential to revoke is registered in + * @param credentialId the id of the credential to revoke (this is a derived public key) * @param nonce the revocation nonce * @param signingKey the signing key associated with the credential (the private key to the {@link credentialId}) - * @returns an update contract transaction for revoking the credential with id {@link credentialId} + * @returns the unserialized parameters for a holder revocation transaction */ -export async function buildRevokeTransaction( +export async function buildRevokeTransactionParameters( address: ContractAddress, - contractName: string, credentialId: string, nonce: bigint, signingKey: string -): Promise { +) { const signingData: SigningData = { contractAddress: address, entryPoint: 'revokeCredentialHolder', @@ -146,13 +144,30 @@ export async function buildRevokeTransaction( data, }; - // Get better NRG estimate. How to do that? Invoke the contract and use that. + return parameter; +} + +/** + * Builds a holder revocation transaction to revoke a given verifiable credential in a CIS-4 contract. + * @param address the address of the contract that the credential to revoke is registered in + * @param contractName the name of the contract at {@link address} + * @param credentialId the id of the credential to revoke + * @param maxContractExecutionEnergy the maximum contract execution energy + * @returns an update contract transaction for revoking the credential with id {@link credentialId} + */ +export async function buildRevokeTransaction( + address: ContractAddress, + contractName: string, + credentialId: string, + maxContractExecutionEnergy: bigint, + parameters: RevokeCredentialHolderParam +): Promise { return { address, amount: new CcdAmount(0n), receiveName: `${contractName}.revokeCredentialHolder`, - maxContractExecutionEnergy: BigInt(50000), - message: serializeRevokeCredentialHolderParam(parameter), + maxContractExecutionEnergy, + message: serializeRevokeCredentialHolderParam(parameters), }; } @@ -172,6 +187,27 @@ function deserializeCredentialStatus(serializedCredentialStatus: string): Verifi } } +/** + * Estimates the cost of a holder revocation transaction. + * @param client the GRPC client for accessing a node + * @param contractName the name of the contract to invoke to get the estimate + * @param parameters the unserialized parameters for a holder revocation transaction + * @returns an estimate of the execution cost of the holder revocation transaction + */ +export async function getRevokeTransactionExecutionEnergyEstimate( + client: ConcordiumGRPCClient, + contractName: string, + parameters: RevokeCredentialHolderParam +) { + const invokeResult = await client.invokeContract({ + contract: parameters.data.signingData.contractAddress, + method: `${contractName}.${parameters.data.signingData.entryPoint}`, + parameter: serializeRevokeCredentialHolderParam(parameters), + }); + // TODO Use a shared function for the buffer calculation. Also used in token-helpers. + return (invokeResult.usedEnergy * 12n) / 10n; +} + /** * Get the status of a verifiable credential in a CIS-4 contract. * @param client the GRPC client for accessing a node From 6985da6cead1ba70e2d588dd2e40fc64e3ec40f5 Mon Sep 17 00:00:00 2001 From: orhoj Date: Wed, 5 Jul 2023 11:40:46 +0200 Subject: [PATCH 07/14] Refactor NRG buffer calculation --- .../src/shared/utils/contract-helpers.ts | 9 +++++++++ .../browser-wallet/src/shared/utils/token-helpers.ts | 5 +++-- .../src/shared/utils/verifiable-credential-helpers.ts | 5 +++-- packages/browser-wallet/test/contract-helpers.test.ts | 11 +++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/browser-wallet/src/shared/utils/contract-helpers.ts create mode 100644 packages/browser-wallet/test/contract-helpers.test.ts diff --git a/packages/browser-wallet/src/shared/utils/contract-helpers.ts b/packages/browser-wallet/src/shared/utils/contract-helpers.ts new file mode 100644 index 00000000..a2916559 --- /dev/null +++ b/packages/browser-wallet/src/shared/utils/contract-helpers.ts @@ -0,0 +1,9 @@ +/** + * Applies a buffer of 20% to an estimated execution energy amount. The goal is to prevent transactions + * from running into insufficient energy errors. + * @param estimatedExecutionEnergy the estimated execution cost for an update transaction, should be retrieved by invoking the contract + * @returns returns the estimated execution energy with an additional buffer added to prevent running into insufficient energy errors + */ +export function applyExecutionNRGBuffer(estimatedExecutionEnergy: bigint) { + return (estimatedExecutionEnergy * 12n) / 10n; +} diff --git a/packages/browser-wallet/src/shared/utils/token-helpers.ts b/packages/browser-wallet/src/shared/utils/token-helpers.ts index 5fa3a707..ff3f63d1 100644 --- a/packages/browser-wallet/src/shared/utils/token-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/token-helpers.ts @@ -15,6 +15,7 @@ import { CIS2_SCHEMA_CONTRACT_NAME, CIS2_SCHEMA } from '@popup/constants/schema' import i18n from '@popup/shell/i18n'; import { SmartContractParameters } from '@concordium/browser-wallet-api-helpers'; import { determineUpdatePayloadSize } from './energy-helpers'; +import { applyExecutionNRGBuffer } from './contract-helpers'; export interface ContractDetails { contractName: string; @@ -347,8 +348,8 @@ async function getTokenTransferExecutionEnergyEstimate( if (!res || res.tag === 'failure') { throw new Error(res?.reason?.tag || 'no response'); } - // TODO: determine the "safety ratio" - return (res.usedEnergy * 12n) / 10n; + + return applyExecutionNRGBuffer(res.usedEnergy); } function getContractName(instanceInfo: InstanceInfo): string | undefined { diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index e115f0c2..42cea93e 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -2,6 +2,7 @@ import { CcdAmount, ConcordiumGRPCClient, ContractAddress, UpdateContractPayload import { VerifiableCredentialStatus } from '@shared/storage/types'; import { Buffer } from 'buffer/'; import * as ed from '@noble/ed25519'; +import { applyExecutionNRGBuffer } from './contract-helpers'; /** * Extracts the credential holder id from a verifiable credential id (did). @@ -204,8 +205,8 @@ export async function getRevokeTransactionExecutionEnergyEstimate( method: `${contractName}.${parameters.data.signingData.entryPoint}`, parameter: serializeRevokeCredentialHolderParam(parameters), }); - // TODO Use a shared function for the buffer calculation. Also used in token-helpers. - return (invokeResult.usedEnergy * 12n) / 10n; + + return applyExecutionNRGBuffer(invokeResult.usedEnergy); } /** diff --git a/packages/browser-wallet/test/contract-helpers.test.ts b/packages/browser-wallet/test/contract-helpers.test.ts new file mode 100644 index 00000000..f41c42c7 --- /dev/null +++ b/packages/browser-wallet/test/contract-helpers.test.ts @@ -0,0 +1,11 @@ +import { applyExecutionNRGBuffer } from '../src/shared/utils/contract-helpers'; + +test('NRG buffer added is 20 percent', () => { + const estimatedExecutionEnergy = 500n; + expect(applyExecutionNRGBuffer(estimatedExecutionEnergy)).toEqual(600n); +}); + +test('NRG buffer returns 0 for 0 input', () => { + const estimatedExecutionEnergy = 0n; + expect(applyExecutionNRGBuffer(estimatedExecutionEnergy)).toEqual(0n); +}); From 34b368adb063ff3b1c40e3a6ad151477bff335a9 Mon Sep 17 00:00:00 2001 From: orhoj Date: Fri, 7 Jul 2023 13:41:12 +0200 Subject: [PATCH 08/14] Update credential entry to match updated contract --- .../VerifiableCredentialDetails.tsx | 2 +- .../VerifiableCredentialHooks.tsx | 4 +- .../src/popup/shared/PopupMenu/PopupMenu.tsx | 1 + .../utils/verifiable-credential-helpers.ts | 53 ++++++++++--------- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx index 7b1da0fa..3b186578 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialDetails.tsx @@ -99,7 +99,7 @@ export default function VerifiableCredentialDetails({ { title: t('menu.revoke'), icon: , - onClick: credentialEntry.holderRevocable ? () => goToConfirmPage() : undefined, + onClick: credentialEntry.credentialInfo.holderRevocable ? () => goToConfirmPage() : undefined, }, ], }; diff --git a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx index 3d207c30..a395511b 100644 --- a/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx +++ b/packages/browser-wallet/src/popup/pages/VerifiableCredential/VerifiableCredentialHooks.tsx @@ -1,7 +1,7 @@ import { grpcClientAtom } from '@popup/store/settings'; import { VerifiableCredential, VerifiableCredentialStatus } from '@shared/storage/types'; import { - CredentialEntry, + CredentialQueryResponse, getCredentialHolderId, getCredentialRegistryContractAddress, getVerifiableCredentialEntry, @@ -16,7 +16,7 @@ import { useEffect, useState } from 'react'; * @returns the credential entry for the given credential, undefined if one is not found yet */ export function useCredentialEntry(credential: VerifiableCredential) { - const [credentialEntry, setCredentialEntry] = useState(); + const [credentialEntry, setCredentialEntry] = useState(); const client = useAtomValue(grpcClientAtom); useEffect(() => { diff --git a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx index d7f158e2..8173b94f 100644 --- a/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx +++ b/packages/browser-wallet/src/popup/shared/PopupMenu/PopupMenu.tsx @@ -21,6 +21,7 @@ export default function PopupMenu({ items, onClickOutside }: PopupMenuProps) { {items.map((item) => { return ( )} diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts index f8f4fd8a..d961ce97 100644 --- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts @@ -141,11 +141,13 @@ export async function buildRevokeTransactionParameters( nonce: bigint, signingKey: string ) { + const fiveMinutesInMilliseconds = 5 * 60000; + const signatureExpirationTimestamp = BigInt(Date.now() + fiveMinutesInMilliseconds); const signingData: SigningData = { contractAddress: address, entryPoint: 'revokeCredentialHolder', nonce, - timestamp: BigInt(Date.now() + 5 * 60000), + timestamp: signatureExpirationTimestamp, }; const data: RevocationDataHolder = {