diff --git a/examples/wallet-api/package.json b/examples/wallet-api/package.json index 204e7c469..3349b113f 100644 --- a/examples/wallet-api/package.json +++ b/examples/wallet-api/package.json @@ -9,7 +9,8 @@ "start:sign-message": "FILE=sign-message.html yarn start", "start:sign-cis3-message": "FILE=sign-cis3-message.html yarn start", "start:web3-id": "FILE=web3-id.html yarn start", - "start:sign-token-update": "FILE=sign-token-update.html yarn start" + "start:sign-token-update": "FILE=sign-token-update.html yarn start", + "start:sponsored-transaction": "FILE=sponsored-transaction.html yarn start" }, "dependencies": { "@concordium/web-sdk": "12.0.0-devnet-p10.0" diff --git a/examples/wallet-api/sign-token-update.html b/examples/wallet-api/sign-token-update.html index 8b55d33b6..f2fecefe4 100644 --- a/examples/wallet-api/sign-token-update.html +++ b/examples/wallet-api/sign-token-update.html @@ -30,8 +30,15 @@ }); document.getElementById('signSingleTransferTokenUpdate').addEventListener('click', () => { - const { TokenOperationType, Cbor, TokenAmount, TokenHolder, CborMemo, TokenId, AccountAddress } = - concordiumSDK; + const { + TokenOperationType, + Cbor, + TokenAmount, + CborAccountAddress, + CborMemo, + TokenId, + AccountAddress, + } = concordiumSDK; const ops = [ { @@ -40,7 +47,7 @@ value: '1000000', decimals: 6, }), - recipient: TokenHolder.fromAccountAddress( + recipient: CborAccountAddress.fromAccountAddress( AccountAddress.fromBase58('4PVJxxtiNVanfuNgvFWtFT1JCiqoVs6npVXBqwLNKprqnUiC2A') ), memo: CborMemo.fromString('Some text for memo'), @@ -62,8 +69,15 @@ }); document.getElementById('signTokenUpdate').addEventListener('click', () => { - const { TokenOperationType, Cbor, TokenAmount, TokenHolder, CborMemo, TokenId, AccountAddress } = - concordiumSDK; + const { + TokenOperationType, + Cbor, + TokenAmount, + CborAccountAddress, + CborMemo, + TokenId, + AccountAddress, + } = concordiumSDK; const ops = [ { @@ -72,7 +86,7 @@ value: '1000000', decimals: 6, }), - recipient: TokenHolder.fromAccountAddress( + recipient: CborAccountAddress.fromAccountAddress( AccountAddress.fromBase58('4PVJxxtiNVanfuNgvFWtFT1JCiqoVs6npVXBqwLNKprqnUiC2A') ), memo: CborMemo.fromString('Some text for memo'), @@ -95,7 +109,7 @@ value: '200000', decimals: 6, }), - recipient: TokenHolder.fromAccountAddress( + recipient: CborAccountAddress.fromAccountAddress( AccountAddress.fromBase58('4PVJxxtiNVanfuNgvFWtFT1JCiqoVs6npVXBqwLNKprqnUiC2A') ), memo: CborMemo.fromString('Some text for memo'), diff --git a/examples/wallet-api/sponsored-transaction.html b/examples/wallet-api/sponsored-transaction.html new file mode 100644 index 000000000..15cdd4a2a --- /dev/null +++ b/examples/wallet-api/sponsored-transaction.html @@ -0,0 +1,162 @@ + + + + My cool dapp + + + + + + + + +
+ +

Account address:

+
+
+ +
+

Sponsor Config:

+ Sponsor Account: + +
+ Sponsor Private Key: + +
+

Recipient Config:

+ Recipient Address: + +
+
+ + + + diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index eba8f2998..65923eb9a 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -25,6 +25,7 @@ import type { UpdateContractInput, UpdateCredentialsInput, VerifiablePresentation, + Transaction, } from '@concordium/web-sdk'; import type { RpcTransport } from '@protobuf-ts/runtime-rpc'; import { LaxNumberEnumValue, LaxStringEnumValue } from './util'; @@ -69,6 +70,8 @@ export type SendTransactionPayload = | SendTransactionUpdateContractPayload | SendTransactionInitContractPayload; +export type SignableTransaction = Transaction.Signable; + export type SmartContractParameters = | { [key: string]: SmartContractParameters } | SmartContractParameters[] @@ -254,6 +257,14 @@ interface MainWalletApi { type: LaxNumberEnumValue, payload: ConfigureDelegationPayload ): Promise; + /** + * Sends a transaction signed by sponsor to the Concordium Wallet and awaits the users action. + * Note that a header is sent, and constructed by the sponsor. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param transaction the sponsored transaction with header to be signed and sent. + */ + sendSponsoredTransaction(accountAddress: AccountAddressSource, transaction: SignableTransaction): Promise; /** * Sends a message to the Concordium Wallet and awaits the users action. If the user signs the message, this will resolve to the signature. * Note that if the user rejects signing the message, this will throw an error. diff --git a/packages/browser-wallet/CHANGELOG.md b/packages/browser-wallet/CHANGELOG.md index 59f933074..d5d292112 100644 --- a/packages/browser-wallet/CHANGELOG.md +++ b/packages/browser-wallet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 2.7.2 + +- Added support for Sponsored Transactions handling. With new method `sendSponsoredTransaction` in wallet-api. + ## 2.7.0 ### Changed diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index e27ba6bba..41e2cad34 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -1,7 +1,7 @@ { "name": "@concordium/browser-wallet", "private": true, - "version": "2.7.0", + "version": "2.7.2", "description": "Browser extension wallet for the Concordium blockchain", "author": "Concordium Software", "license": "Apache-2.0", diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts index d1d58b635..452d3f16d 100644 --- a/packages/browser-wallet/src/background/index.ts +++ b/packages/browser-wallet/src/background/index.ts @@ -531,6 +531,14 @@ forwardToPopup( undefined, withPromptEnd ); +forwardToPopup( + MessageType.SendSponsoredTransaction, + InternalMessageType.SendSponsoredTransaction, + runConditionComposer(runIfAccountIsAllowlisted, async () => ({ run: true }), withPromptStart()), + appendUrlToPayload, + undefined, + withPromptEnd +); forwardToPopup( MessageType.SignMessage, InternalMessageType.SignMessage, diff --git a/packages/browser-wallet/src/messaging/message.ts b/packages/browser-wallet/src/messaging/message.ts index b06f5d18d..f94e303e5 100644 --- a/packages/browser-wallet/src/messaging/message.ts +++ b/packages/browser-wallet/src/messaging/message.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; */ export enum MessageType { SendTransaction = 'M_SendTransaction', + SendSponsoredTransaction = 'M_SendSponsoredTransaction', SignMessage = 'M_SignMessage', GetAccounts = 'M_GetAccounts', GetSelectedAccount = 'M_GetSelectedAccount', @@ -29,6 +30,7 @@ export enum InternalMessageType { Init = 'I_Init', PopupReady = 'I_PopupReady', SendTransaction = 'I_SendTransaction', + SendSponsoredTransaction = 'I_SendSponsoredTransaction', SignMessage = 'I_SignMessage', Connect = 'I_Connect', TestPopupOpen = 'I_TestPopupOpen', diff --git a/packages/browser-wallet/src/popup/constants/routes.ts b/packages/browser-wallet/src/popup/constants/routes.ts index 2a52f5335..47cdaf4aa 100644 --- a/packages/browser-wallet/src/popup/constants/routes.ts +++ b/packages/browser-wallet/src/popup/constants/routes.ts @@ -6,6 +6,8 @@ type RouteChildren = { [key: string]: RouteNode | RoutePath; }; +// ToDo sendSponsoredTransaction not implemented in OldUI +// Need redirect to new UI, or Sunset OldUI export const relativeRoutes = { home: { path: '/', @@ -63,6 +65,9 @@ export const relativeRoutes = { sendTransaction: { path: 'send-transaction', }, + sendSponsoredTransaction: { + path: 'send-sponsored-transaction', + }, endIdentityIssuance: { path: 'end-identity-issuance', }, diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index 6d55e07f6..b1f5b37b7 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -410,6 +410,9 @@ export const relativeRoutes = { sendTransaction: { path: 'sendTransaction', }, + sendSponsoredTransaction: { + path: 'sendSponsoredTransaction', + }, addWeb3IdCredential: { path: 'add-web3id-credential', }, diff --git a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/Confirm.tsx b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/Confirm.tsx index cde31dccd..6d14ebd05 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/Confirm.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/Confirm.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Cbor, CborMemo, TokenAmount, TokenId, TokenOperationType, TokenHolder } from '@concordium/web-sdk/plt'; +import { Cbor, CborMemo, TokenAmount, TokenId, TokenOperationType, CborAccountAddress } from '@concordium/web-sdk/plt'; import { AccountAddress, AccountTransactionType, @@ -116,7 +116,7 @@ export default function SendFundsConfirm({ values, fee, sender }: Props) { value: parseTokenAmount(values.amount, tokenMetadata?.decimals).toString(), decimals: tokenMetadata?.decimals || 0, }), - recipient: TokenHolder.fromAccountAddress(receiver), + recipient: CborAccountAddress.fromAccountAddress(receiver), memo: values.memo ? CborMemo.fromString(values.memo) : undefined, }, }, diff --git a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendFunds.tsx b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendFunds.tsx index 9b4906454..8faa3e46c 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendFunds.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendFunds.tsx @@ -18,7 +18,7 @@ import { TokenId, TokenAmount as TokenAmountPlt, TokenOperationType, - TokenHolder, + CborAccountAddress, } from '@concordium/web-sdk/plt'; import { useAsyncMemo } from 'wallet-common-helpers'; import { useAtomValue } from 'jotai'; @@ -104,7 +104,7 @@ function SendFunds({ address }: SendFundsProps) { value: parseTokenAmount(amount, metadata?.decimals).toString(), decimals: metadata?.decimals || 0, }), - recipient: TokenHolder.fromAccountAddress(address), + recipient: CborAccountAddress.fromAccountAddress(address), memo: memo ? CborMemo.fromString(memo) : undefined, }, }, diff --git a/packages/browser-wallet/src/popup/popupX/pages/prompts/SendSponsoredTransaction/DisplaySingleTransferTokenUpdate.tsx b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendSponsoredTransaction/DisplaySingleTransferTokenUpdate.tsx new file mode 100644 index 000000000..af4eb9e30 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/prompts/SendSponsoredTransaction/DisplaySingleTransferTokenUpdate.tsx @@ -0,0 +1,346 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + AccountInfo, + HexString, + isRejectTransaction, + isSuccessTransaction, + TokenUpdatePayload, + TransactionHash, + TransactionSummaryType, +} from '@concordium/web-sdk'; +import { Cbor, CborMemo, TokenOperationType, TokenTransferOperation } from '@concordium/web-sdk/plt'; +import { WalletCredential } from '@shared/storage/types'; +import { displaySplitAddress, useCredential, useIdentityName } from '@popup/shared/utils/account-helpers'; +import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext'; +import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; +import { displayUrl } from '@popup/shared/utils/string-helpers'; +import { logError } from '@shared/utils/log-helpers'; +import { LoaderInline } from '@popup/popupX/shared/Loader'; +import { fullscreenPromptContext } from '@popup/popupX/page-layouts/FullscreenPromptLayout'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { addToastAtom } from '@popup/state'; +import { grpcClientAtom } from '@popup/store/settings'; +import { useAsyncMemo } from 'wallet-common-helpers'; +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import Card from '@popup/popupX/shared/Card'; +import Button from '@popup/popupX/shared/Button'; +import Label from '@popup/popupX/shared/Label'; +import Tooltip from '@popup/popupX/shared/Tooltip/Tooltip'; +import QuestionIcon from '@assets/svgX/UiKit/Interface/circled-question-mark.svg'; +import CheckCircle from '@assets/svgX/check-circle.svg'; +import Cross from '@assets/svgX/close.svg'; + +interface Status { + type?: 'success' | 'failure'; +} + +function TransactionStatusIcon({ status }: { status?: Status }) { + const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.sponsored' }); + switch (status?.type) { + case undefined: { + return ( + <> + + {t('pending.label')} + + ); + } + case 'success': { + return ( + <> + + {t('success.label')} + + ); + } + case 'failure': { + return ( + <> + + {t('failure.label')} + + ); + } + default: + throw new Error('Unexpected status'); + } +} + +type TransactionStatusProps = { + status?: Status; + amount?: string | number; + sponsorAccount?: string; + tokenName?: string; +}; + +function TransactionStatus({ status, amount, sponsorAccount, tokenName }: TransactionStatusProps) { + const { t } = useTranslation('x', { keyPrefix: 'prompts.sendTransactionX.sponsored' }); + + return ( + <> + + {t('tokenNameAmount', { tokenName })} + {amount} + {t('transactionFee')}: +