diff --git a/examples/angular/src/app/components/content/content.component.html b/examples/angular/src/app/components/content/content.component.html index 88ca7ffb4..43e57a47c 100644 --- a/examples/angular/src/app/components/content/content.component.html +++ b/examples/angular/src/app/components/content/content.component.html @@ -2,6 +2,7 @@
+ diff --git a/examples/angular/src/app/components/content/content.component.ts b/examples/angular/src/app/components/content/content.component.ts index 62ed199bf..6bd0e71f2 100644 --- a/examples/angular/src/app/components/content/content.component.ts +++ b/examples/angular/src/app/components/content/content.component.ts @@ -111,6 +111,21 @@ export class ContentComponent implements OnInit, OnDestroy { alert("Switched account to " + nextAccountId); } + async onVerifyOwner() { + const wallet = await this.selector.wallet(); + try { + const signature = await wallet.verifyOwner(); + + if (signature) { + alert(`Signature for verification: ${signature.signature.toString()}`); + } + } catch (err) { + const message = + err instanceof Error ? err.message : "Something went wrong"; + alert(message); + } + } + subscribeToEvents() { this.subscription = this.selector.store.observable .pipe( diff --git a/examples/react/components/Content.tsx b/examples/react/components/Content.tsx index 9b14c73a9..919d571f6 100644 --- a/examples/react/components/Content.tsx +++ b/examples/react/components/Content.tsx @@ -164,6 +164,21 @@ const Content: React.FC = () => { [selector, accountId] ); + const handleVerifyOwner = async () => { + const wallet = await selector.wallet(); + try { + const signature = await wallet.verifyOwner(); + + if (signature) { + alert(`Signature for verification: ${signature.signature.toString()}`); + } + } catch (err) { + const message = + err instanceof Error ? err.message : "Something went wrong"; + alert(message); + } + }; + const handleSubmit = useCallback( async (e: SubmitEvent) => { e.preventDefault(); @@ -220,6 +235,7 @@ const Content: React.FC = () => {
+ {accounts.length > 1 && ( )} diff --git a/packages/core/docs/api/wallet.md b/packages/core/docs/api/wallet.md index 6291dc1dd..a36300a67 100644 --- a/packages/core/docs/api/wallet.md +++ b/packages/core/docs/api/wallet.md @@ -182,6 +182,37 @@ Returns one or more accounts when signed in. This method can be useful for walle })(); ``` +### `.verifyOwner(params)` + +**Parameters** +- `params` (`object`) + - `message` (`string?`): The message requested sign. Defaults to `verify owner` string. + - `signerId` (`string?`): Account ID used to sign the message. Defaults to the first account. + - `publicKey` (`PublicKey?`): Public key used to sign the message. Defaults to the public key of the signed in account. + - `callbackUrl` (`string?`): Applicable to browser wallets (e.g. MyNearWallet). This is the callback url once the signing is approved. Defaults to `window.location.href`. + - `meta` (`string?`): Applicable to browser wallets (e.g. MyNearWallet) extra data that will be passed to the callback url once the signing is approved. + +**Returns** +- `Promise`: Browser wallets won't return the signing outcome as they may need to redirect for signing. For MyNearWallet the outcome is passed to the callback url. + +**Description** + +Signs the message and verifies the owner. Message is not sent to blockchain. + +> Note: This feature is currently supported only by MyNearWallet on **testnet**. Sender can sign messages when unlocked. + +**Example** + +```ts +// MyNearWallet +(async () => { + const wallet = await selector.wallet("my-near-wallet"); + await wallet.verifyOwner({ + message: "Test message", + }); +})(); +``` + ### `.signAndSendTransaction(params)` **Parameters** diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 435409d31..65d133c5b 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -1,4 +1,4 @@ -import { providers } from "near-api-js"; +import { providers, utils } from "near-api-js"; import { EventEmitterService, @@ -10,6 +10,7 @@ import type { Options } from "../options.types"; import type { ReadOnlyStore } from "../store.types"; import type { Transaction, Action } from "./transactions.types"; import type { Modify, Optional } from "../utils.types"; +import { PublicKey } from "near-api-js/lib/utils"; import type { FinalExecutionOutcome } from "near-api-js/lib/providers"; interface BaseWalletMetadata { @@ -29,6 +30,14 @@ export interface SignInParams { methodNames?: Array; } +export interface VerifyOwnerParams { + message?: string; + signerId?: string; + publicKey?: PublicKey; + callbackUrl?: string; + meta?: string; +} + export interface SignAndSendTransactionParams { signerId?: string; receiverId?: string; @@ -43,6 +52,9 @@ interface BaseWalletBehaviour { signIn(params: SignInParams): Promise>; signOut(): Promise; getAccounts(): Promise>; + verifyOwner( + params?: VerifyOwnerParams + ): Promise; signAndSendTransaction( params: SignAndSendTransactionParams ): Promise; diff --git a/packages/ledger/src/lib/ledger.ts b/packages/ledger/src/lib/ledger.ts index 101dfd88b..9653346a7 100644 --- a/packages/ledger/src/lib/ledger.ts +++ b/packages/ledger/src/lib/ledger.ts @@ -60,6 +60,7 @@ const Ledger: WalletBehaviourFactory = async ({ provider, logger, storage, + metadata, }) => { const _state = await setupLedgerState(storage); @@ -219,6 +220,41 @@ const Ledger: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("Ledger:verifyOwner", { message, signerId, publicKey }); + + const account = getActiveAccount(store.getState()); + + if (!account) { + throw new Error("No active account"); + } + + // Note: Connection must be triggered by user interaction. + await connectLedgerDevice(); + + const networkId = options.network.networkId; + const accountId = signerId || account.accountId; + const pubKey = + publicKey || (await signer.getPublicKey(accountId, networkId)); + const block = await provider.block({ finality: "final" }); + + const msg = JSON.stringify({ + accountId, + message, + blockId: block.header.hash, + publicKey: Buffer.from(pubKey.data).toString("base64"), + keyType: pubKey.keyType, + }); + + throw new Error(`Method not supported by ${metadata.name}`); + + return signer.signMessage( + new Uint8Array(Buffer.from(msg)), + accountId, + networkId + ); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index ce0a4a11f..ff5b0e2dd 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -41,6 +41,7 @@ const setupMathWalletState = (): MathWalletState => { }; const MathWallet: WalletBehaviourFactory = async ({ + metadata, options, store, provider, @@ -104,6 +105,38 @@ const MathWallet: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("MathWallet:verifyOwner", { message, signerId, publicKey }); + + const account = getActiveAccount(store.getState()); + + if (!account) { + throw new Error("No active account"); + } + + const accountId = signerId || account.accountId; + const pubKey = + publicKey || (await _state.wallet.signer.getPublicKey(accountId)); + const block = await provider.block({ finality: "final" }); + + const msg = JSON.stringify({ + accountId, + message, + blockId: block.header.hash, + publicKey: Buffer.from(pubKey.data).toString("base64"), + keyType: pubKey.keyType, + }); + + // Note: Math Wallet currently hangs when calling signMessage. + throw new Error(`Method not supported by ${metadata.name}`); + + return _state.wallet.signer.signMessage( + new Uint8Array(Buffer.from(msg)), + accountId, + options.network.networkId + ); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); const signedTransactions = await signTransactions( diff --git a/packages/meteor-wallet/src/lib/meteor-wallet.ts b/packages/meteor-wallet/src/lib/meteor-wallet.ts index c01f57f24..e0e90fff0 100644 --- a/packages/meteor-wallet/src/lib/meteor-wallet.ts +++ b/packages/meteor-wallet/src/lib/meteor-wallet.ts @@ -48,7 +48,7 @@ const setupWalletState = async ( const createMeteorWalletInjected: WalletBehaviourFactory< InjectedWallet, { params: MeteorWalletParams_Injected } -> = async ({ options, logger, store, params }) => { +> = async ({ metadata, options, logger, store, params }) => { const _state = await setupWalletState(params, options.network); const cleanup = () => { @@ -155,6 +155,12 @@ const createMeteorWalletInjected: WalletBehaviourFactory< return getAccounts(); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("MeteorWallet:verifyOwner", { message, signerId, publicKey }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("MeteorWallet:signAndSendTransaction", { signerId, diff --git a/packages/my-near-wallet/src/lib/my-near-wallet.ts b/packages/my-near-wallet/src/lib/my-near-wallet.ts index d723dd15e..f6e772412 100644 --- a/packages/my-near-wallet/src/lib/my-near-wallet.ts +++ b/packages/my-near-wallet/src/lib/my-near-wallet.ts @@ -74,7 +74,7 @@ const setupWalletState = async ( const MyNearWallet: WalletBehaviourFactory< BrowserWallet, { params: MyNearWalletExtraOptions } -> = async ({ options, store, params, logger }) => { +> = async ({ metadata, options, store, params, logger }) => { const _state = await setupWalletState(params, options.network); const cleanup = () => { @@ -155,6 +155,39 @@ const MyNearWallet: WalletBehaviourFactory< return getAccounts(); }, + async verifyOwner({ + message = "verify owner", + signerId, + publicKey, + callbackUrl, + meta, + } = {}) { + logger.log("verifyOwner", { message, signerId, publicKey }); + + const account = _state.wallet.account(); + + if (!account) { + throw new Error("Wallet not signed in"); + } + const locationUrl = + typeof window !== "undefined" ? window.location.href : ""; + + const url = callbackUrl || locationUrl; + + if (!url) { + throw new Error(`The callbackUrl is missing for ${metadata.name}`); + } + + const encodedUrl = encodeURIComponent(url); + const extraMeta = meta ? `&meta=${meta}` : ""; + + window.location.replace( + `${params.walletUrl}/verify-owner?message=${message}&callbackUrl=${encodedUrl}${extraMeta}` + ); + + return; + }, + async signAndSendTransaction({ signerId, receiverId, diff --git a/packages/nightly-connect/src/lib/nightly-connect.ts b/packages/nightly-connect/src/lib/nightly-connect.ts index 5cf380ecc..6331e5779 100644 --- a/packages/nightly-connect/src/lib/nightly-connect.ts +++ b/packages/nightly-connect/src/lib/nightly-connect.ts @@ -44,7 +44,7 @@ const setupNightlyConnectState = (): NightlyConnectState => { const NightlyConnect: WalletBehaviourFactory< BridgeWallet, { params: NightlyConnectParams } -> = async ({ store, params, logger, options, provider, emitter }) => { +> = async ({ metadata, store, params, logger, options, provider, emitter }) => { const _state = setupNightlyConnectState(); const getAccounts = () => { @@ -156,6 +156,16 @@ const NightlyConnect: WalletBehaviourFactory< return getAccounts().map(({ accountId }) => ({ accountId })); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("NightlyConnect:verifyOwner", { + message, + signerId, + publicKey, + }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); diff --git a/packages/nightly/src/lib/nightly.ts b/packages/nightly/src/lib/nightly.ts index 67703d8ed..91afbc3fa 100644 --- a/packages/nightly/src/lib/nightly.ts +++ b/packages/nightly/src/lib/nightly.ts @@ -40,6 +40,7 @@ const isInstalled = () => { return waitFor(() => !!window.nightly!.near!).catch(() => false); }; const Nightly: WalletBehaviourFactory = async ({ + metadata, options, store, logger, @@ -141,6 +142,12 @@ const Nightly: WalletBehaviourFactory = async ({ return getAccounts().map(({ accountId }) => ({ accountId })); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("Nightly:verifyOwner", { message, signerId, publicKey }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); diff --git a/packages/sender/src/lib/injected-sender.ts b/packages/sender/src/lib/injected-sender.ts index 6ad3aa201..b208ab65e 100644 --- a/packages/sender/src/lib/injected-sender.ts +++ b/packages/sender/src/lib/injected-sender.ts @@ -1,7 +1,7 @@ // Interfaces based on "documentation": https://github.com/SenderWallet/sender-wallet-integration-tutorial // Empty string if we haven't signed in before. -import { providers } from "near-api-js"; +import { Account, providers } from "near-api-js"; interface AccessKey { publicKey: { @@ -113,6 +113,7 @@ export interface InjectedSender { isSender: boolean; callbacks: Record; getAccountId: () => string | null; + account(): Account | null; getRpc: () => Promise; requestSignIn: ( params: RequestSignInParams diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index a64b0fd31..9e4986dad 100644 --- a/packages/sender/src/lib/sender.ts +++ b/packages/sender/src/lib/sender.ts @@ -42,6 +42,7 @@ const Sender: WalletBehaviourFactory = async ({ options, metadata, store, + provider, emitter, logger, }) => { @@ -174,6 +175,44 @@ const Sender: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("Sender:verifyOwner", { message, signerId, publicKey }); + + const account = _state.wallet.account(); + + if (!account) { + throw new Error("Wallet not signed in"); + } + + // Note: When the wallet is locked, Sender returns an empty Signer interface. + // Even after unlocking the wallet, the user will need to refresh to gain + // access to these methods. + if (!account.connection.signer.signMessage) { + throw new Error("Wallet is locked"); + } + + const networkId = options.network.networkId; + const accountId = signerId || account.accountId; + const pubKey = + publicKey || + (await account.connection.signer.getPublicKey(accountId, networkId)); + const block = await provider.block({ finality: "final" }); + + const msg = JSON.stringify({ + accountId, + message, + blockId: block.header.hash, + publicKey: Buffer.from(pubKey.data).toString("base64"), + keyType: pubKey.keyType, + }); + + return account.connection.signer.signMessage( + new Uint8Array(Buffer.from(msg)), + accountId, + networkId + ); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index b12a33094..2dca5dcf2 100644 --- a/packages/wallet-connect/src/lib/wallet-connect.ts +++ b/packages/wallet-connect/src/lib/wallet-connect.ts @@ -66,7 +66,7 @@ const setupWalletConnectState = async ( const WalletConnect: WalletBehaviourFactory< BridgeWallet, { params: WalletConnectExtraOptions } -> = async ({ options, store, params, emitter, logger }) => { +> = async ({ metadata, options, store, params, emitter, logger }) => { const _state = await setupWalletConnectState(params); const getChainId = () => { @@ -177,6 +177,12 @@ const WalletConnect: WalletBehaviourFactory< return getAccounts(); }, + async verifyOwner({ message = "verify owner", signerId, publicKey } = {}) { + logger.log("WalletConnect:verifyOwner", { message, signerId, publicKey }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions });