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 });