diff --git a/.vscode/settings.json b/.vscode/settings.json
index a4a8998a1..ab2d991bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,8 +2,7 @@
"eslint.format.enable": true,
"prettier.enable": false,
"editor.codeActionsOnSave": {
- "source.fixAll.eslint":
- true
+ "source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib"
}
diff --git a/examples/angular/src/app/components/content/content.component.html b/examples/angular/src/app/components/content/content.component.html
index d7263f413..bb273a517 100644
--- a/examples/angular/src/app/components/content/content.component.html
+++ b/examples/angular/src/app/components/content/content.component.html
@@ -20,5 +20,6 @@
+
diff --git a/examples/angular/src/app/components/content/content.component.ts b/examples/angular/src/app/components/content/content.component.ts
index 31b010090..3d5079e56 100644
--- a/examples/angular/src/app/components/content/content.component.ts
+++ b/examples/angular/src/app/components/content/content.component.ts
@@ -112,6 +112,13 @@ export class ContentComponent implements OnInit, OnDestroy {
this.modal.show();
}
+ signInMessage() {
+ const message = "test message to sign";
+ const nonce = Buffer.from(Array.from(Array(32).keys()));
+ const recipient = "guest-book.testnet";
+ this.modal.signInMessage({ message, nonce, recipient });
+ }
+
async signOut() {
const wallet = await this.selector.wallet();
@@ -211,14 +218,14 @@ export class ContentComponent implements OnInit, OnDestroy {
const publicKey = urlParams.get("publicKey") as string;
const signature = urlParams.get("signature") as string;
- if (!accId && !publicKey && !signature) {
- return;
- }
-
const message: SignMessageParams = JSON.parse(
localStorage.getItem("message") as string
);
+ if ((!accId && !publicKey && !signature) || !message) {
+ return;
+ }
+
const signedMessage = {
accountId: accId,
publicKey,
@@ -328,7 +335,7 @@ export class ContentComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
signerId: this.accountId!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- receiverId: contract!.contractId,
+ receiverId: contract?.contractId || CONTRACT_ID,
actions: [
{
type: "FunctionCall",
diff --git a/examples/react/components/Content.tsx b/examples/react/components/Content.tsx
index 271442fa9..0efb00039 100644
--- a/examples/react/components/Content.tsx
+++ b/examples/react/components/Content.tsx
@@ -135,6 +135,13 @@ const Content: React.FC = () => {
modal.show();
};
+ const handleSignInMessage = () => {
+ const message = "test message to sign";
+ const nonce = Buffer.from(Array.from(Array(32).keys()));
+ const recipient = "guest-book.testnet";
+ modal.signInMessage({ message, nonce, recipient });
+ };
+
const handleSignOut = async () => {
const wallet = await selector.wallet();
@@ -192,7 +199,7 @@ const Content: React.FC = () => {
for (let i = 0; i < 2; i += 1) {
transactions.push({
signerId: accountId!,
- receiverId: contract!.contractId,
+ receiverId: contract?.contractId || CONTRACT_ID,
actions: [
{
type: "FunctionCall",
@@ -275,14 +282,14 @@ const Content: React.FC = () => {
const publicKey = urlParams.get("publicKey") as string;
const signature = urlParams.get("signature") as string;
- if (!accId && !publicKey && !signature) {
- return;
- }
-
const message: SignMessageParams = JSON.parse(
localStorage.getItem("message")!
);
+ if ((!accId && !publicKey && !signature) || !message) {
+ return;
+ }
+
const signedMessage = {
accountId: accId,
publicKey,
@@ -378,6 +385,7 @@ const Content: React.FC = () => {
+
diff --git a/packages/core/docs/api/selector.md b/packages/core/docs/api/selector.md
index 818dd2e8f..e84a1bf75 100644
--- a/packages/core/docs/api/selector.md
+++ b/packages/core/docs/api/selector.md
@@ -153,6 +153,29 @@ Programmatically change active account which will be used to sign and send trans
selector.setActiveAccount("sometestaccount.testnet");
```
+### `.signInType()`
+
+**Parameters**
+
+- N/A
+
+**Returns**
+
+- `SignInType`
+
+**Description**
+
+Programmatically check the sign-in type, if signed-in with `wallet.signIn(params)` returns "key" if signed in with `wallet.signInMessage(params)` returns "message"
+
+> Note: This function will throw when calling without being signed in.
+
+**Example**
+
+```ts
+const signInType = selector.signInType();
+
+```
+
### `.on(event, callback)`
**Parameters**
@@ -183,7 +206,7 @@ subscription.remove();
**Parameters**
-- `event` (`string`): Name of the event. This can be: `signedIn | signedOut | accountsChanged | networkChanged | uriChanged`.
+- `event` (`string`): Name of the event. This can be: `signedIn | signedInMessage | signedOut | accountsChanged | networkChanged | uriChanged`.
- `callback` (`Function`): Original handler passed to `.on(event, callback)`.
**Returns**
diff --git a/packages/core/docs/api/state.md b/packages/core/docs/api/state.md
index f9537acf4..f1cd4accf 100644
--- a/packages/core/docs/api/state.md
+++ b/packages/core/docs/api/state.md
@@ -93,3 +93,45 @@ Returns ID-s of 5 recently signed in wallets.
const { recentlySignedInWallets } = selector.store.getState();
console.log(recentlySignedInWallets); // ["my-near-wallet", "sender", ...]
```
+
+### `.message`
+
+**Returns**
+
+- `SignInMessageParams | null`
+ - `message` (`string`): The message that wants to be transmitted.
+ - `nonce` (`Buffer`): A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
+ - `recipient` (`string`): The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
+ - `callbackUrl` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process.
+ - `state` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). A state for authentication purposes.
+
+
+**Description**
+
+Returns the original message that was used for `signInMessage`.
+
+**Example**
+
+```ts
+const { message } = selector.store.getState();
+console.log(message); // { message: "test", nonce: [0...31], recipient: "myapp.com" }
+```
+
+### `.signedInMessageAccount`
+
+**Returns**
+
+- `Account | null`
+ - `accountId` (`string`): The account name to which the publicKey corresponds as plain text (e.g. "alice.near").
+ - `publicKey` (`string`): The public counterpart of the key used to sign, expressed as a string with format ":" (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y")
+
+**Description**
+
+Returns the `Account` that was signed with `signInMessage`.
+
+**Example**
+
+```ts
+const { signedMessage } = selector.store.getState();
+console.log(signedMessage); // { accountId: "alice.near", publicKey: "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y" }
+```
diff --git a/packages/core/docs/api/wallet.md b/packages/core/docs/api/wallet.md
index c7eb30170..9de6af43b 100644
--- a/packages/core/docs/api/wallet.md
+++ b/packages/core/docs/api/wallet.md
@@ -326,3 +326,36 @@ Allows users to sign a message for a specific recipient using their NEAR account
await wallet.signMessage({ message, recipient, nonce });
})();
```
+
+### `.signInMessage(params)`
+
+**Parameters**
+- `params` (`object`)
+ - `message` (`string`): The message that wants to be transmitted.
+ - `nonce` (`Buffer`): A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
+ - `recipient` (`string`): The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
+ - `callbackUrl` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`.
+ - `state` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). A state for authentication purposes.
+
+**Returns**
+- `Promise`
+
+**Description**
+
+Allows users to sign-in (login) to a dApp without creating a LAK by signing a message for a specific recipient using their NEAR account, based on the [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md).
+
+**Example**
+
+```ts
+// MyNearWallet
+(async () => {
+ const wallet = await selector.wallet("my-near-wallet");
+ const message = "test message for verification";
+ let nonceArray: Uint8Array = new Uint8Array(32);
+ nonceArray = crypto.getRandomValues(nonceArray);
+ const nonce = Buffer.from(nonceArray);
+ const recipient = "myapp.com";
+
+ await wallet.signInMessage({ message, nonce, recipient });
+})();
+```
diff --git a/packages/core/docs/guides/custom-wallets.md b/packages/core/docs/guides/custom-wallets.md
index 59d27f9d2..69b5e95a3 100644
--- a/packages/core/docs/guides/custom-wallets.md
+++ b/packages/core/docs/guides/custom-wallets.md
@@ -74,6 +74,12 @@ const MyWallet: WalletBehaviourFactory = ({
// that allows users to sign a message for a specific receiver using their NEAR account
return await wallet.signMessage({ message, nonce, recipient, callbackUrl, state });
},
+ async signInMessage({ message, nonce, recipient, callbackUrl, state }) {
+ // Sign in to My Wallet withotut creating a LAK for access to account(s).
+ // Signs the message, verifies it and returns the `SignedMessage`.
+
+ return [];
+ },
};
};
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 5c827337b..23fd58eff 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -3,6 +3,7 @@ export type {
WalletSelectorParams,
WalletSelectorEvents,
WalletSelectorStore,
+ SignInType,
} from "./lib/wallet-selector.types";
export { setupWalletSelector } from "./lib/wallet-selector";
@@ -70,6 +71,7 @@ export type {
AccountImportData,
SignedMessage,
SignMessageParams,
+ SignInMessageParams,
} from "./lib/wallet";
export type { FinalExecutionOutcome } from "near-api-js/lib/providers";
@@ -81,6 +83,7 @@ export {
verifyFullKeyBelongsToUser,
verifySignature,
serializeNep413,
+ verifyMessageNEP413,
} from "./lib/helpers";
export { translate, allowOnlyLanguage } from "./lib/translate/translate";
diff --git a/packages/core/src/lib/constants.ts b/packages/core/src/lib/constants.ts
index b7d204beb..586c0d7fc 100644
--- a/packages/core/src/lib/constants.ts
+++ b/packages/core/src/lib/constants.ts
@@ -6,3 +6,7 @@ export const PENDING_CONTRACT = "contract:pending";
export const SELECTED_WALLET_ID = `selectedWalletId`;
export const PENDING_SELECTED_WALLET_ID = `selectedWalletId:pending`;
+
+export const SIGN_IN_MESSAGE = "message";
+export const PENDING_SIGN_IN_MESSAGE = "message:pending";
+export const SIGNED_IN_MESSAGE_ACCOUNT = "signedInMessageAccount";
diff --git a/packages/core/src/lib/helpers/verify-signature/verify-signature.ts b/packages/core/src/lib/helpers/verify-signature/verify-signature.ts
index 9487cc09e..8ff77046e 100644
--- a/packages/core/src/lib/helpers/verify-signature/verify-signature.ts
+++ b/packages/core/src/lib/helpers/verify-signature/verify-signature.ts
@@ -8,6 +8,8 @@ import type {
} from "./verify-signature.types";
import { Payload, payloadSchema } from "./payload";
import type { AccessKeyView } from "near-api-js/lib/providers/provider";
+import type { SignedMessage, SignMessageParams } from "../../wallet";
+import type { Network } from "../../options.types";
export const verifySignature = ({
publicKey,
@@ -64,3 +66,34 @@ export const verifyFullKeyBelongsToUser = async ({
return permission === "FullAccess";
};
+
+export const verifyMessageNEP413 = async (
+ message: SignMessageParams,
+ signedMessage: SignedMessage,
+ network: Network
+) => {
+ const isSignatureValid = verifySignature({
+ message: message.message,
+ nonce: message.nonce,
+ recipient: message.recipient,
+ publicKey: signedMessage.publicKey,
+ signature: signedMessage.signature,
+ callbackUrl: message.callbackUrl,
+ });
+
+ if (!isSignatureValid) {
+ throw Error("Failed to verify the signature");
+ }
+
+ const isFullAccess = await verifyFullKeyBelongsToUser({
+ publicKey: signedMessage.publicKey,
+ accountId: signedMessage.accountId,
+ network,
+ });
+
+ if (!isFullAccess) {
+ throw Error("Message was not signed with full access key");
+ }
+
+ return true;
+};
diff --git a/packages/core/src/lib/services/wallet-modules/wallet-modules.service.ts b/packages/core/src/lib/services/wallet-modules/wallet-modules.service.ts
index 61f37f585..b8d14ccf8 100644
--- a/packages/core/src/lib/services/wallet-modules/wallet-modules.service.ts
+++ b/packages/core/src/lib/services/wallet-modules/wallet-modules.service.ts
@@ -20,10 +20,11 @@ import {
PACKAGE_NAME,
PENDING_CONTRACT,
PENDING_SELECTED_WALLET_ID,
+ PENDING_SIGN_IN_MESSAGE,
} from "../../constants";
import { JsonStorage } from "../storage/json-storage.service";
import type { ProviderService } from "../provider/provider.service.types";
-import type { SignMessageMethod } from "../../wallet";
+import type { SignMessageMethod, SignInMessageParams } from "../../wallet";
export class WalletModules {
private factories: Array;
@@ -82,11 +83,21 @@ export class WalletModules {
PENDING_CONTRACT
);
- if (pendingSelectedWalletId && pendingContract) {
+ const pendingSignInMessage = await jsonStorage.getItem(
+ `${pendingSelectedWalletId}:${PENDING_SIGN_IN_MESSAGE}`
+ );
+
+ if (
+ (pendingSelectedWalletId && pendingContract) ||
+ (pendingSelectedWalletId && pendingSignInMessage)
+ ) {
const accounts = await this.validateWallet(pendingSelectedWalletId);
await jsonStorage.removeItem(PENDING_SELECTED_WALLET_ID);
await jsonStorage.removeItem(PENDING_CONTRACT);
+ await jsonStorage.removeItem(
+ `${pendingSelectedWalletId}:${PENDING_SIGN_IN_MESSAGE}`
+ );
if (accounts.length) {
const { selectedWalletId } = this.store.getState();
@@ -107,11 +118,14 @@ export class WalletModules {
contract: pendingContract,
selectedWalletId: pendingSelectedWalletId,
recentlySignedInWallets: recentlySignedInWalletsFromPending,
+ message: pendingSignInMessage,
+ signedInMessageAccount: accounts[0],
};
}
}
- const { contract, selectedWalletId } = this.store.getState();
+ const { contract, selectedWalletId, message, signedInMessageAccount } =
+ this.store.getState();
const accounts = await this.validateWallet(selectedWalletId);
const recentlySignedInWallets = await jsonStorage.getItem>(
@@ -124,6 +138,8 @@ export class WalletModules {
contract: null,
selectedWalletId: null,
recentlySignedInWallets: recentlySignedInWallets || [],
+ message: null,
+ signedInMessageAccount: null,
};
}
@@ -132,6 +148,8 @@ export class WalletModules {
contract,
selectedWalletId,
recentlySignedInWallets: recentlySignedInWallets || [],
+ message,
+ signedInMessageAccount,
};
}
@@ -200,7 +218,14 @@ export class WalletModules {
this.store.dispatch({
type: "WALLET_CONNECTED",
- payload: { walletId, contract, accounts, recentlySignedInWallets },
+ payload: {
+ walletId,
+ contract,
+ accounts,
+ recentlySignedInWallets,
+ message: null,
+ signedInMessageAccount: null,
+ },
});
this.emitter.emit("signedIn", {
@@ -211,6 +236,60 @@ export class WalletModules {
});
}
+ private async onWalletSignedInMessage(
+ walletId: string,
+ { accounts, message, signedMessage }: WalletEvents["signedInMessage"]
+ ) {
+ const { selectedWalletId } = this.store.getState();
+ const jsonStorage = new JsonStorage(this.storage, PACKAGE_NAME);
+
+ if (!accounts.length) {
+ const module = this.getModule(walletId)!;
+ // We can't guarantee the user will actually sign in with browser wallets.
+ // Best we can do is set in storage and validate on init.
+ if (module.type === "browser") {
+ const locationUrl =
+ typeof window !== "undefined" ? window.location.href : undefined;
+ await jsonStorage.setItem(PENDING_SELECTED_WALLET_ID, walletId);
+ await jsonStorage.setItem(`${module.id}:${PENDING_SIGN_IN_MESSAGE}`, {
+ ...message,
+ callbackUrl: message.callbackUrl || locationUrl,
+ });
+ }
+
+ return;
+ }
+
+ if (selectedWalletId && selectedWalletId !== walletId) {
+ await this.signOutWallet(selectedWalletId);
+ }
+
+ const recentlySignedInWallets = await this.setWalletAsRecentlySignedIn(
+ walletId
+ );
+
+ const { accountId, publicKey } = signedMessage;
+
+ this.store.dispatch({
+ type: "WALLET_CONNECTED",
+ payload: {
+ walletId,
+ contract: null,
+ accounts,
+ recentlySignedInWallets,
+ message,
+ signedInMessageAccount: { accountId, publicKey },
+ },
+ });
+
+ this.emitter.emit("signedInMessage", {
+ walletId,
+ message,
+ signedMessage,
+ accounts,
+ });
+ }
+
private onWalletSignedOut(walletId: string) {
this.store.dispatch({
type: "WALLET_DISCONNECTED",
@@ -231,6 +310,10 @@ export class WalletModules {
this.onWalletSignedIn(module.id, event);
});
+ emitter.on("signedInMessage", (event) => {
+ this.onWalletSignedInMessage(module.id, event);
+ });
+
emitter.on("accountsChanged", async ({ accounts }) => {
this.emitter.emit("accountsChanged", { walletId: module.id, accounts });
@@ -279,6 +362,7 @@ export class WalletModules {
const _signIn = wallet.signIn;
const _signOut = wallet.signOut;
const _signMessage = wallet.signMessage;
+ const _signInMessage = wallet.signInMessage;
wallet.signIn = async (params: never) => {
const accounts = await _signIn(params);
@@ -310,6 +394,33 @@ export class WalletModules {
return await _signMessage(params);
};
+ wallet.signInMessage = async (params: never) => {
+ if (_signInMessage === undefined) {
+ throw Error(
+ `The signInMessage method is not supported by ${wallet.metadata.name}`
+ );
+ }
+
+ const message = params as SignInMessageParams;
+ const signedInMessage = await _signInMessage(message);
+ const accounts: Array = [];
+
+ if (signedInMessage) {
+ accounts.push({
+ accountId: signedInMessage.accountId,
+ publicKey: signedInMessage.publicKey,
+ });
+ }
+
+ await this.onWalletSignedInMessage(wallet.id, {
+ accounts,
+ message,
+ signedMessage: signedInMessage!,
+ });
+
+ return signedInMessage;
+ };
+
return wallet;
}
@@ -411,8 +522,14 @@ export class WalletModules {
this.modules = modules;
- const { accounts, contract, selectedWalletId, recentlySignedInWallets } =
- await this.resolveStorageState();
+ const {
+ accounts,
+ contract,
+ selectedWalletId,
+ recentlySignedInWallets,
+ message,
+ signedInMessageAccount,
+ } = await this.resolveStorageState();
this.store.dispatch({
type: "SETUP_WALLET_MODULES",
@@ -422,6 +539,8 @@ export class WalletModules {
contract,
selectedWalletId,
recentlySignedInWallets,
+ message: message,
+ signedInMessageAccount,
},
});
diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts
index 48c22ee82..ae0b12911 100644
--- a/packages/core/src/lib/store.ts
+++ b/packages/core/src/lib/store.ts
@@ -12,6 +12,8 @@ import {
CONTRACT,
SELECTED_WALLET_ID,
RECENTLY_SIGNED_IN_WALLETS,
+ SIGN_IN_MESSAGE,
+ SIGNED_IN_MESSAGE_ACCOUNT,
} from "./constants";
const reducer = (
@@ -28,6 +30,8 @@ const reducer = (
contract,
selectedWalletId,
recentlySignedInWallets,
+ message,
+ signedInMessageAccount,
} = action.payload;
const accountStates = accounts.map((account, i) => {
@@ -44,11 +48,19 @@ const reducer = (
contract,
selectedWalletId,
recentlySignedInWallets,
+ message: message,
+ signedInMessageAccount,
};
}
case "WALLET_CONNECTED": {
- const { walletId, contract, accounts, recentlySignedInWallets } =
- action.payload;
+ const {
+ walletId,
+ contract,
+ accounts,
+ recentlySignedInWallets,
+ message,
+ signedInMessageAccount,
+ } = action.payload;
if (!accounts.length) {
return state;
@@ -71,6 +83,8 @@ const reducer = (
accounts: accountStates,
selectedWalletId: walletId,
recentlySignedInWallets,
+ message,
+ signedInMessageAccount,
};
}
case "WALLET_DISCONNECTED": {
@@ -85,6 +99,8 @@ const reducer = (
contract: null,
accounts: [],
selectedWalletId: null,
+ message: null,
+ signedInMessageAccount: null,
};
}
case "ACCOUNTS_CHANGED": {
@@ -143,6 +159,10 @@ export const createStore = async (storage: StorageService): Promise => {
selectedWalletId: await jsonStorage.getItem(SELECTED_WALLET_ID),
recentlySignedInWallets:
(await jsonStorage.getItem(RECENTLY_SIGNED_IN_WALLETS)) || [],
+ message: await jsonStorage.getItem(SIGN_IN_MESSAGE),
+ signedInMessageAccount: await jsonStorage.getItem(
+ SIGNED_IN_MESSAGE_ACCOUNT
+ ),
};
const state$ = new BehaviorSubject(initialState);
@@ -178,6 +198,13 @@ export const createStore = async (storage: StorageService): Promise => {
RECENTLY_SIGNED_IN_WALLETS,
"recentlySignedInWallets"
);
+ syncStorage(prevState, state, SIGN_IN_MESSAGE, "message");
+ syncStorage(
+ prevState,
+ state,
+ SIGNED_IN_MESSAGE_ACCOUNT,
+ "signedInMessageAccount"
+ );
prevState = state;
});
diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts
index a5696a146..5463efa4b 100644
--- a/packages/core/src/lib/store.types.ts
+++ b/packages/core/src/lib/store.types.ts
@@ -1,7 +1,11 @@
import type { BehaviorSubject, Observable } from "rxjs";
-import type { Wallet, Account } from "./wallet";
-import type { SignMessageMethod } from "./wallet";
+import type {
+ Wallet,
+ Account,
+ SignInMessageParams,
+ SignMessageMethod,
+} from "./wallet";
export interface ContractState {
/**
@@ -61,6 +65,14 @@ export interface WalletSelectorState {
* Returns ID-s of 5 recently signed in wallets.
*/
recentlySignedInWallets: Array;
+ /**
+ * The original message that was used to "sign-in" via signInMessages
+ */
+ message: SignInMessageParams | null;
+ /**
+ * The Account that was used to sign the message
+ */
+ signedInMessageAccount: Account | null;
}
export type WalletSelectorAction =
@@ -72,15 +84,19 @@ export type WalletSelectorAction =
contract: ContractState | null;
selectedWalletId: string | null;
recentlySignedInWallets: Array;
+ message: SignInMessageParams | null;
+ signedInMessageAccount: Account | null;
};
}
| {
type: "WALLET_CONNECTED";
payload: {
walletId: string;
- contract: ContractState;
+ contract: ContractState | null;
accounts: Array;
recentlySignedInWallets: Array;
+ message: SignInMessageParams | null;
+ signedInMessageAccount: Account | null;
};
}
| {
diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts
index 0ea7a36d1..c9d320ce4 100644
--- a/packages/core/src/lib/wallet-selector.ts
+++ b/packages/core/src/lib/wallet-selector.ts
@@ -54,6 +54,14 @@ const createSelector = (
return Boolean(accounts.length);
},
+ signInType() {
+ const { contract, signedInMessageAccount } = store.getState();
+
+ if (!contract && !signedInMessageAccount) {
+ throw Error("Wallet not signed in");
+ }
+ return contract ? "key" : "message";
+ },
on: (eventName, callback) => {
return emitter.on(eventName, callback);
},
diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts
index a50079c14..552f51d29 100644
--- a/packages/core/src/lib/wallet-selector.types.ts
+++ b/packages/core/src/lib/wallet-selector.types.ts
@@ -8,6 +8,9 @@ import type { Network, NetworkId, Options } from "./options.types";
import type { Subscription, StorageService } from "./services";
import type { SupportedLanguage } from "./translate/translate";
import type { SignMessageMethod } from "./wallet/wallet.types";
+import type { SignedMessage, SignInMessageParams } from "./wallet/wallet.types";
+
+export type SignInType = "key" | "message";
export interface WalletSelectorParams {
/**
@@ -57,6 +60,12 @@ export type WalletSelectorEvents = {
methodNames: Array;
accounts: Array;
};
+ signedInMessage: {
+ walletId: string;
+ message: SignInMessageParams;
+ signedMessage: SignedMessage;
+ accounts: Array;
+ };
signedOut: {
walletId: string;
};
@@ -94,6 +103,11 @@ export interface WalletSelector {
*/
setActiveAccount(accountId: string): void;
+ /**
+ * Returns the sign-in type, it can be either "message" or "key".
+ */
+ signInType(): SignInType;
+
/**
* Attach an event handler to important events.
*/
diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts
index 586e4c1bc..f0cd5f2a6 100644
--- a/packages/core/src/lib/wallet/wallet.types.ts
+++ b/packages/core/src/lib/wallet/wallet.types.ts
@@ -99,6 +99,14 @@ export type SignMessageMethod = {
signMessage(params: SignMessageParams): Promise;
};
+export interface SignInMessageParams {
+ message: string;
+ recipient: string;
+ nonce: Buffer;
+ callbackUrl?: string;
+ state?: string;
+}
+
interface SignAndSendTransactionParams {
/**
* Account ID used to sign the transaction. Defaults to the first account.
@@ -154,7 +162,16 @@ interface BaseWalletBehaviour {
signAndSendTransactions(
params: SignAndSendTransactionsParams
): Promise>;
+
+ /**
+ * Signs a message for a specific recipient using their NEAR account, based on the [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md).
+ */
signMessage?(params: SignMessageParams): Promise;
+ /**
+ * Signs a message for a specific recipient using their NEAR account, based on the [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md).
+ * Sets the wallet selector's isSignedIn state to true after the message is signed and verified.
+ */
+ signInMessage?(params: SignInMessageParams): Promise;
}
type BaseWallet<
@@ -182,6 +199,11 @@ export type WalletEvents = {
methodNames: Array;
accounts: Array;
};
+ signedInMessage: {
+ message: SignInMessageParams;
+ signedMessage: SignedMessage;
+ accounts: Array;
+ };
signedOut: null;
accountsChanged: { accounts: Array };
networkChanged: { networkId: string };
diff --git a/packages/here-wallet/src/lib/selector.ts b/packages/here-wallet/src/lib/selector.ts
index 6d810183a..fc047895d 100644
--- a/packages/here-wallet/src/lib/selector.ts
+++ b/packages/here-wallet/src/lib/selector.ts
@@ -1,4 +1,5 @@
-import type { NetworkId } from "@near-wallet-selector/core";
+import type { Account, NetworkId } from "@near-wallet-selector/core";
+import { verifyMessageNEP413 } from "@near-wallet-selector/core";
import { HereWallet } from "@here-wallet/core";
import type BN from "bn.js";
@@ -18,19 +19,28 @@ export const initHereWallet: SelectorInit = async (config) => {
async function getAccounts() {
logger.log("HereWallet:getAccounts");
const accountIds = await here.getAccounts();
- const accounts = [];
-
- for (let i = 0; i < accountIds.length; i++) {
- accounts.push({
- accountId: accountIds[i],
- publicKey: (
- await here.signer.getPublicKey(
- accountIds[i],
- options.network.networkId
- )
- ).toString(),
- });
+ const accounts: Array = [];
+ const { signedInMessageAccount } = store.getState();
+
+ if (accountIds.length > 0) {
+ for (let i = 0; i < accountIds.length; i++) {
+ accounts.push({
+ accountId: accountIds[i],
+ publicKey: (
+ await here.signer.getPublicKey(
+ accountIds[i],
+ options.network.networkId
+ )
+ ).toString(),
+ });
+ }
+ return accounts;
}
+
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
+ }
+
return accounts;
}
@@ -89,8 +99,10 @@ export const initHereWallet: SelectorInit = async (config) => {
},
async signOut() {
- logger.log("HereWallet:signOut");
- await here.signOut();
+ if (await here.isSignedIn()) {
+ logger.log("HereWallet:signOut");
+ await here.signOut();
+ }
},
async getAccounts() {
@@ -122,8 +134,31 @@ export const initHereWallet: SelectorInit = async (config) => {
return await here.signMessage(data);
},
+ async signInMessage(data) {
+ logger.log("HereWallet:signInMessage", data);
+
+ const signedMessage = await here.signMessage(data);
+
+ const isMessageVerified = await verifyMessageNEP413(
+ data,
+ signedMessage,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
+ }
+
+ return signedMessage;
+ },
+
async signAndSendTransactions(data) {
logger.log("HereWallet:signAndSendTransactions", data);
+ const { contract } = store.getState();
+ if (!here.isSignedIn || !contract) {
+ throw new Error("Wallet not signed in");
+ }
+
return await here.signAndSendTransactions(data);
},
};
diff --git a/packages/meteor-wallet/src/lib/meteor-wallet.ts b/packages/meteor-wallet/src/lib/meteor-wallet.ts
index f0c7f0b15..342eb9be7 100644
--- a/packages/meteor-wallet/src/lib/meteor-wallet.ts
+++ b/packages/meteor-wallet/src/lib/meteor-wallet.ts
@@ -5,6 +5,7 @@ import type {
WalletBehaviourFactory,
WalletModuleFactory,
} from "@near-wallet-selector/core";
+import { verifyMessageNEP413 } from "@near-wallet-selector/core";
import type {
MeteorWalletParams_Injected,
MeteorWalletState,
@@ -48,21 +49,26 @@ const createMeteorWalletInjected: WalletBehaviourFactory<
const getAccounts = async (): Promise> => {
const accountId = _state.wallet.getAccountId();
const account = _state.wallet.account();
+ const { signedInMessageAccount } = store.getState();
+
+ if (accountId && account) {
+ const publicKey = await account.connection.signer.getPublicKey(
+ account.accountId,
+ options.network.networkId
+ );
+ return [
+ {
+ accountId,
+ publicKey: publicKey ? publicKey.toString() : "",
+ },
+ ];
+ }
- if (!accountId || !account) {
- return [];
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
}
- const publicKey = await account.connection.signer.getPublicKey(
- account.accountId,
- options.network.networkId
- );
- return [
- {
- accountId,
- publicKey: publicKey ? publicKey.toString() : "",
- },
- ];
+ return [];
};
return {
@@ -150,6 +156,30 @@ const createMeteorWalletInjected: WalletBehaviourFactory<
}
},
+ async signInMessage(message) {
+ logger.log("MeteorWallet:signInMessage", message);
+ const accountId = _state.wallet.getAccountId();
+ const response = await _state.wallet.signMessage({
+ ...message,
+ accountId,
+ });
+ if (response.success) {
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ response.payload,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
+ }
+
+ return response.payload;
+ } else {
+ throw new Error(`Couldn't sign message owner: ${response.message}`);
+ }
+ },
+
async signAndSendTransaction({ signerId, receiverId, actions }) {
logger.log("MeteorWallet:signAndSendTransaction", {
signerId,
diff --git a/packages/modal-ui-js/docs/api/modal.md b/packages/modal-ui-js/docs/api/modal.md
index 16e66bbe4..e7aab1dae 100644
--- a/packages/modal-ui-js/docs/api/modal.md
+++ b/packages/modal-ui-js/docs/api/modal.md
@@ -40,6 +40,36 @@ Closes the modal.
modal.hide();
```
+### `.signInMessage(params)`
+
+**Parameters**
+
+- `params` (`object`)
+ - `message` (`string`): The message that wants to be transmitted.
+ - `nonce` (`Buffer`): A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
+ - `recipient` (`string`): The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
+ - `callbackUrl` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`.
+ - `state` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). A state for authentication purposes.
+
+
+**Returns**
+
+- `void`
+
+**Description**
+
+Opens the modal for users to sign in to their preferred wallet with `signInMessage` [here](../../../core/docs/api/wallet.md#signinmessageparams).
+
+**Example**
+
+```ts
+const message = "test message to sign";
+const nonce = Buffer.from(Array.from(Array(32).keys()));
+const recipient = "guest-book.testnet";
+
+modal.signInMessage({ message, nonce, recipient });
+```
+
### `.on(event, callback)`
diff --git a/packages/modal-ui-js/src/lib/components/LedgerAccountsOverviewList.ts b/packages/modal-ui-js/src/lib/components/LedgerAccountsOverviewList.ts
index d5f7b6bed..7a01c0b6e 100644
--- a/packages/modal-ui-js/src/lib/components/LedgerAccountsOverviewList.ts
+++ b/packages/modal-ui-js/src/lib/components/LedgerAccountsOverviewList.ts
@@ -69,14 +69,20 @@ export async function renderLedgerAccountsOverviewList(
}
const wallet = await module.wallet();
- wallet.signIn({
- contractId: modalState.options.contractId,
- methodNames: modalState.options.methodNames,
- accounts: selectedAccounts,
- });
+
+ if (modalState.message) {
+ await wallet.signInMessage!(modalState.message);
+ } else {
+ await wallet.signIn({
+ contractId: modalState.options.contractId,
+ methodNames: modalState.options.methodNames,
+ accounts: selectedAccounts,
+ });
+ }
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "wallet-navigation" });
+ modalState.message = undefined;
} catch (err) {
await renderWalletConnectionFailed(module, err as Error);
}
diff --git a/packages/modal-ui-js/src/lib/modal.ts b/packages/modal-ui-js/src/lib/modal.ts
index 668a5e3fa..6fc681579 100644
--- a/packages/modal-ui-js/src/lib/modal.ts
+++ b/packages/modal-ui-js/src/lib/modal.ts
@@ -1,6 +1,7 @@
import type {
EventEmitterService,
ModuleState,
+ SignInMessageParams,
Wallet,
WalletSelector,
} from "@near-wallet-selector/core";
@@ -28,6 +29,7 @@ type ModalState = {
modules: Array>;
derivationPath: string;
emitter: EventEmitterService;
+ message?: SignInMessageParams;
};
export let modalState: ModalState | null = null;
@@ -99,36 +101,46 @@ export const setupModal = (
}
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "user-triggered" });
+ modalState.message = undefined;
}
};
window.addEventListener("keydown", close);
renderModal();
+ const showUI = (state: ModalState) => {
+ allowOnlyLanguage(state.selector.options.languageCode);
+ renderModal();
+ const selectedWalletId = state.selector.store.getState().selectedWalletId;
+ if (selectedWalletId) {
+ const module = state.modules.find((m) => m.id === selectedWalletId);
+ renderWalletAccount(module);
+ } else {
+ renderWhatIsAWallet();
+ }
+ state.container.children[0].classList.add("open");
+ };
+
if (!modalInstance) {
modalInstance = {
show: () => {
if (!modalState) {
return;
}
- allowOnlyLanguage(modalState.selector.options.languageCode);
- renderModal();
- const selectedWalletId =
- modalState.selector.store.getState().selectedWalletId;
- if (selectedWalletId) {
- const module = modalState.modules.find(
- (m) => m.id === selectedWalletId
- );
- renderWalletAccount(module);
- } else {
- renderWhatIsAWallet();
+ showUI(modalState);
+ },
+ signInMessage(message: SignInMessageParams) {
+ if (!modalState) {
+ return;
}
- modalState.container.children[0].classList.add("open");
+ modalState.message = message;
+ showUI(modalState);
},
hide: () => {
if (!modalState) {
return;
}
+ modalState.message = undefined;
modalState.container.children[0].classList.remove("open");
},
on: (eventName, callback) => {
diff --git a/packages/modal-ui-js/src/lib/modal.types.ts b/packages/modal-ui-js/src/lib/modal.types.ts
index 714f4f8e5..8acc39718 100644
--- a/packages/modal-ui-js/src/lib/modal.types.ts
+++ b/packages/modal-ui-js/src/lib/modal.types.ts
@@ -1,4 +1,4 @@
-import type { Wallet } from "@near-wallet-selector/core";
+import type { SignInMessageParams, Wallet } from "@near-wallet-selector/core";
import type { ModuleState } from "@near-wallet-selector/core";
import type { Subscription } from "@near-wallet-selector/core";
@@ -20,6 +20,7 @@ export type ModalEvents = {
export interface WalletSelectorModal {
show(): void;
hide(): void;
+ signInMessage(message: SignInMessageParams): void;
on(
eventName: EventName,
callback: (event: ModalEvents[EventName]) => void
diff --git a/packages/modal-ui-js/src/lib/render-modal.ts b/packages/modal-ui-js/src/lib/render-modal.ts
index f33968ee4..585b32ff5 100644
--- a/packages/modal-ui-js/src/lib/render-modal.ts
+++ b/packages/modal-ui-js/src/lib/render-modal.ts
@@ -82,7 +82,6 @@ export async function connectToWallet(
if (selectedWalletId === module.id) {
renderWalletAccount(module);
- return;
}
try {
@@ -116,48 +115,63 @@ export async function connectToWallet(
}
if (wallet.type === "bridge") {
- const subscription = modalState.selector.on("uriChanged", ({ uri }) => {
- renderScanQRCode(module, {
- uri,
- handleOpenDefaultModal: () => {
- connectToWallet(module, true);
- },
+ if (modalState.message) {
+ await wallet.signInMessage!(modalState.message);
+ } else {
+ const subscription = modalState.selector.on("uriChanged", ({ uri }) => {
+ renderScanQRCode(module, {
+ uri,
+ handleOpenDefaultModal: () => {
+ connectToWallet(module, true);
+ },
+ });
});
- });
- await wallet.signIn({
- contractId: modalState.options.contractId,
- methodNames: modalState.options.methodNames,
- qrCodeModal,
- });
+ await wallet.signIn({
+ contractId: modalState.options.contractId,
+ methodNames: modalState.options.methodNames,
+ qrCodeModal,
+ });
+
+ subscription.remove();
+ }
- subscription.remove();
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "wallet-navigation" });
+ modalState.message = undefined;
return;
}
if (wallet.type === "browser") {
- await wallet.signIn({
- contractId: modalState.options.contractId,
- methodNames: modalState.options.methodNames,
- successUrl: wallet.metadata.successUrl,
- failureUrl: wallet.metadata.failureUrl,
- });
+ if (modalState.message) {
+ await wallet.signInMessage!(modalState.message);
+ } else {
+ await wallet.signIn({
+ contractId: modalState.options.contractId,
+ methodNames: modalState.options.methodNames,
+ successUrl: wallet.metadata.successUrl,
+ failureUrl: wallet.metadata.failureUrl,
+ });
+ }
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "wallet-navigation" });
-
+ modalState.message = undefined;
return;
}
- await wallet.signIn({
- contractId: modalState.options.contractId,
- methodNames: modalState.options.methodNames,
- });
+ if (modalState.message) {
+ await wallet.signInMessage!(modalState.message);
+ } else {
+ await wallet.signIn({
+ contractId: modalState.options.contractId,
+ methodNames: modalState.options.methodNames,
+ });
+ }
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "wallet-navigation" });
+ modalState.message = undefined;
} catch (err) {
const { name } = module.metadata;
const message =
@@ -325,6 +339,7 @@ export function renderModal() {
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "user-triggered" });
+ modalState.message = undefined;
});
// TODO: Better handle `click` event listener for close-button.
@@ -340,6 +355,7 @@ export function renderModal() {
modalState.container.children[0].classList.remove("open");
modalState.emitter.emit("onHide", { hideReason: "user-triggered" });
+ modalState.message = undefined;
}
});
initialRender = false;
diff --git a/packages/modal-ui/docs/api/modal.md b/packages/modal-ui/docs/api/modal.md
index 9a81b627a..de9aadcee 100644
--- a/packages/modal-ui/docs/api/modal.md
+++ b/packages/modal-ui/docs/api/modal.md
@@ -40,6 +40,36 @@ Closes the modal.
modal.hide();
```
+### `.signInMessage(params)`
+
+**Parameters**
+
+- `params` (`object`)
+ - `message` (`string`): The message that wants to be transmitted.
+ - `nonce` (`Buffer`): A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array (a fixed `Buffer` in JS/TS).
+ - `recipient` (`string`): The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com").
+ - `callbackUrl` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). The URL to call after the signing process. Defaults to `window.location.href`.
+ - `state` (`string?`): Optional, applicable to browser wallets (e.g. MyNearWallet). A state for authentication purposes.
+
+
+**Returns**
+
+- `void`
+
+**Description**
+
+Opens the modal for users to sign in to their preferred wallet with `signInMessage` [here](../../../core/docs/api/wallet.md#signinmessageparams).
+
+**Example**
+
+```ts
+const message = "test message to sign";
+const nonce = Buffer.from(Array.from(Array(32).keys()));
+const recipient = "guest-book.testnet";
+
+modal.signInMessage({ message, nonce, recipient });
+```
+
### `.on(event, callback)`
**Parameters**
diff --git a/packages/modal-ui/src/lib/components/DerivationPath.tsx b/packages/modal-ui/src/lib/components/DerivationPath.tsx
index 621f7a1e2..1bdc1dd92 100644
--- a/packages/modal-ui/src/lib/components/DerivationPath.tsx
+++ b/packages/modal-ui/src/lib/components/DerivationPath.tsx
@@ -2,6 +2,7 @@ import React, { Fragment, useState } from "react";
import type {
HardwareWallet,
HardwareWalletAccount,
+ SignInMessageParams,
Wallet,
WalletSelector,
} from "@near-wallet-selector/core";
@@ -22,8 +23,9 @@ interface DerivationPathProps {
onBack: () => void;
onConnected: () => void;
params: DerivationPathModalRouteParams;
- onError: (message: string, wallet: Wallet) => void;
+ onError: (errorMessage: string, wallet: Wallet) => void;
onCloseModal: () => void;
+ message: SignInMessageParams | null;
}
export type HardwareWalletAccountState = HardwareWalletAccount & {
@@ -48,6 +50,7 @@ export const DerivationPath: React.FC = ({
params,
onError,
onCloseModal,
+ message,
}) => {
const [route, setRoute] = useState("EnterDerivationPath");
const [derivationPath, setDerivationPath] = useState(
@@ -143,12 +146,12 @@ export const DerivationPath: React.FC = ({
}
} catch (err) {
setConnecting(false);
- const message =
+ const errorMessage =
err && typeof err === "object" && "message" in err
? (err as { message: string }).message
: "Something went wrong";
- onError(message, wallet);
+ onError(errorMessage, wallet);
} finally {
setConnecting(false);
}
@@ -175,11 +178,11 @@ export const DerivationPath: React.FC = ({
setRoute("OverviewAccounts");
} catch (err) {
setConnecting(false);
- const message =
+ const errorMessage =
err && typeof err === "object" && "message" in err
? (err as { message: string }).message
: "Something went wrong";
- onError(message, hardwareWallet!);
+ onError(errorMessage, hardwareWallet!);
} finally {
setConnecting(false);
}
@@ -196,16 +199,24 @@ export const DerivationPath: React.FC = ({
}
);
- return hardwareWallet!
- .signIn({
- contractId: options.contractId,
- methodNames: options.methodNames,
- accounts: mapAccounts,
- })
- .then(() => onConnected())
- .catch((err) => {
- onError(`Error: ${err.message}`, hardwareWallet!);
- });
+ if (message) {
+ return hardwareWallet!.signInMessage!(message)
+ .then(() => onConnected())
+ .catch((err) => {
+ onError(`Error: ${err.message}`, hardwareWallet!);
+ });
+ } else {
+ return hardwareWallet!
+ .signIn({
+ contractId: options.contractId,
+ methodNames: options.methodNames,
+ accounts: mapAccounts,
+ })
+ .then(() => onConnected())
+ .catch((err) => {
+ onError(`Error: ${err.message}`, hardwareWallet!);
+ });
+ }
};
const handleOnBackButtonClick = () => {
diff --git a/packages/modal-ui/src/lib/components/Modal.tsx b/packages/modal-ui/src/lib/components/Modal.tsx
index afcce63da..2ddc59099 100644
--- a/packages/modal-ui/src/lib/components/Modal.tsx
+++ b/packages/modal-ui/src/lib/components/Modal.tsx
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
import type {
EventEmitterService,
ModuleState,
+ SignInMessageParams,
WalletSelector,
} from "@near-wallet-selector/core";
@@ -30,6 +31,7 @@ interface ModalProps {
visible: boolean;
hide: () => void;
emitter: EventEmitterService;
+ message: SignInMessageParams | null;
}
const getThemeClass = (theme?: Theme) => {
@@ -49,6 +51,7 @@ export const Modal: React.FC = ({
visible,
hide,
emitter,
+ message,
}) => {
const [route, setRoute] = useState({
name: "WalletHome",
@@ -137,7 +140,6 @@ export const Modal: React.FC = ({
module,
},
});
- return;
}
try {
@@ -193,11 +195,15 @@ export const Modal: React.FC = ({
});
});
- await wallet.signIn({
- contractId: options.contractId,
- methodNames: options.methodNames,
- qrCodeModal,
- });
+ if (message) {
+ await wallet.signInMessage!(message);
+ } else {
+ await wallet.signIn({
+ contractId: options.contractId,
+ methodNames: options.methodNames,
+ qrCodeModal,
+ });
+ }
subscription.remove();
handleDismissClick({ hideReason: "wallet-navigation" });
@@ -205,33 +211,41 @@ export const Modal: React.FC = ({
}
if (wallet.type === "browser") {
- await wallet.signIn({
- contractId: options.contractId,
- methodNames: options.methodNames,
- successUrl: wallet.metadata.successUrl,
- failureUrl: wallet.metadata.failureUrl,
- });
+ if (message) {
+ await wallet.signInMessage!(message);
+ } else {
+ await wallet.signIn({
+ contractId: options.contractId,
+ methodNames: options.methodNames,
+ successUrl: wallet.metadata.successUrl,
+ failureUrl: wallet.metadata.failureUrl,
+ });
+ }
handleDismissClick({ hideReason: "wallet-navigation" });
return;
}
- await wallet.signIn({
- contractId: options.contractId,
- methodNames: options.methodNames,
- });
+ if (message) {
+ await wallet.signInMessage!(message);
+ } else {
+ await wallet.signIn({
+ contractId: options.contractId,
+ methodNames: options.methodNames,
+ });
+ }
handleDismissClick({ hideReason: "wallet-navigation" });
} catch (err) {
const { name } = module.metadata;
- const message =
+ const errorMessage =
err && typeof err === "object" && "message" in err
? (err as { message: string }).message
: "Something went wrong";
- setAlertMessage(`Failed to sign in with ${name}: ${message}`);
+ setAlertMessage(`Failed to sign in with ${name}: ${errorMessage}`);
setRoute({
name: "AlertMessage",
params: {
@@ -302,13 +316,13 @@ export const Modal: React.FC = ({
name: "WalletHome",
})
}
- onError={(message, wallet) => {
+ onError={(errorMessage, wallet) => {
const { modules } = selector.store.getState();
const findModule = modules.find(
(module) => module.id === wallet.id
);
- setAlertMessage(message);
+ setAlertMessage(errorMessage);
setRoute({
name: "AlertMessage",
params: {
@@ -319,6 +333,7 @@ export const Modal: React.FC = ({
onCloseModal={() =>
handleDismissClick({ hideReason: "user-triggered" })
}
+ message={message}
/>
)}
{route.name === "WalletNetworkChanged" && (
diff --git a/packages/modal-ui/src/lib/modal.tsx b/packages/modal-ui/src/lib/modal.tsx
index 8e8550581..82fde05f6 100644
--- a/packages/modal-ui/src/lib/modal.tsx
+++ b/packages/modal-ui/src/lib/modal.tsx
@@ -1,7 +1,10 @@
import React from "react";
import type { Root } from "react-dom/client";
import { createRoot } from "react-dom/client";
-import type { WalletSelector } from "@near-wallet-selector/core";
+import type {
+ SignInMessageParams,
+ WalletSelector,
+} from "@near-wallet-selector/core";
import type { WalletSelectorModal, ModalOptions } from "./modal.types";
import { Modal } from "./components/Modal";
@@ -34,7 +37,10 @@ export const setupModal = (
const emitter = new EventEmitter();
- const render = (visible = false) => {
+ const render = (
+ visible = false,
+ message: SignInMessageParams | null = null
+ ) => {
root!.render(
render(false)}
emitter={emitter}
+ message={message}
/>
);
};
@@ -54,6 +61,9 @@ export const setupModal = (
hide: () => {
render(false);
},
+ signInMessage(message: SignInMessageParams) {
+ render(true, message);
+ },
on: (eventName, callback) => {
return emitter.on(eventName, callback);
},
diff --git a/packages/modal-ui/src/lib/modal.types.ts b/packages/modal-ui/src/lib/modal.types.ts
index f9c97f978..77206e825 100644
--- a/packages/modal-ui/src/lib/modal.types.ts
+++ b/packages/modal-ui/src/lib/modal.types.ts
@@ -1,4 +1,7 @@
-import type { Subscription } from "@near-wallet-selector/core";
+import type {
+ SignInMessageParams,
+ Subscription,
+} from "@near-wallet-selector/core";
export type Theme = "dark" | "light" | "auto";
@@ -27,6 +30,7 @@ export interface WalletSelectorModal {
/**
* Attach an event handler to important events.
*/
+ signInMessage(params: SignInMessageParams): void;
on(
eventName: EventName,
callback: (event: ModalEvents[EventName]) => void
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 5a3721567..4e294586e 100644
--- a/packages/my-near-wallet/src/lib/my-near-wallet.ts
+++ b/packages/my-near-wallet/src/lib/my-near-wallet.ts
@@ -7,7 +7,11 @@ import type {
Optional,
Network,
Account,
+ JsonStorageService,
+ SignInMessageParams,
+ SignMessageParams,
} from "@near-wallet-selector/core";
+import { verifyMessageNEP413 } from "@near-wallet-selector/core";
import { createAction } from "@near-wallet-selector/wallet-utils";
import icon from "./icon";
@@ -22,6 +26,7 @@ export interface MyNearWalletParams {
interface MyNearWalletState {
wallet: nearAPI.WalletConnection;
keyStore: nearAPI.keyStores.BrowserLocalStorageKeyStore;
+ signedInMessageAccount: Account | null;
}
interface MyNearWalletExtraOptions {
@@ -43,9 +48,49 @@ const resolveWalletUrl = (network: Network, walletUrl?: string) => {
}
};
+const getSignedInMessageAccount = async (
+ storage: JsonStorageService,
+ network: Network
+): Promise => {
+ const urlParams = new URLSearchParams(
+ window.location.hash.substring(1) // skip the first char (#)
+ );
+ const accountId = urlParams.get("accountId") as string;
+ const publicKey = urlParams.get("publicKey") as string;
+ const signature = urlParams.get("signature") as string;
+ let message = await storage.getItem("message:pending");
+
+ if ((!accountId && !publicKey && !signature) || !message) {
+ return null;
+ }
+
+ message = { ...message, nonce: Buffer.from(message.nonce) };
+ const signedMessage = {
+ accountId,
+ publicKey,
+ signature,
+ };
+
+ try {
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ signedMessage,
+ network
+ );
+ if (!isMessageVerified) {
+ return null;
+ }
+
+ return { accountId, publicKey };
+ } catch (_e) {
+ return null;
+ }
+};
+
const setupWalletState = async (
params: MyNearWalletExtraOptions,
- network: Network
+ network: Network,
+ storage: JsonStorageService
): Promise => {
const keyStore = new nearAPI.keyStores.BrowserLocalStorageKeyStore();
@@ -58,35 +103,50 @@ const setupWalletState = async (
const wallet = new nearAPI.WalletConnection(near, "near_app");
+ const signedInMessageAccount = await getSignedInMessageAccount(
+ storage,
+ network
+ );
+
return {
wallet,
keyStore,
+ signedInMessageAccount,
};
};
const MyNearWallet: WalletBehaviourFactory<
BrowserWallet,
{ params: MyNearWalletExtraOptions }
-> = async ({ metadata, options, store, params, logger, id }) => {
- const _state = await setupWalletState(params, options.network);
+> = async ({ metadata, options, store, params, logger, id, storage }) => {
+ const _state = await setupWalletState(params, options.network, storage);
const getAccounts = async (): Promise> => {
const accountId = _state.wallet.getAccountId();
const account = _state.wallet.account();
+ const { signedInMessageAccount } = store.getState();
+
+ if (accountId && account) {
+ const publicKey = await account.connection.signer.getPublicKey(
+ account.accountId,
+ options.network.networkId
+ );
+ return [
+ {
+ accountId,
+ publicKey: publicKey ? publicKey.toString() : "",
+ },
+ ];
+ }
- if (!accountId || !account) {
- return [];
+ if (_state.signedInMessageAccount) {
+ return [{ ..._state.signedInMessageAccount }];
}
- const publicKey = await account.connection.signer.getPublicKey(
- account.accountId,
- options.network.networkId
- );
- return [
- {
- accountId,
- publicKey: publicKey ? publicKey.toString() : "",
- },
- ];
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
+ }
+
+ return [];
};
const transformTransactions = async (
@@ -128,6 +188,35 @@ const MyNearWallet: WalletBehaviourFactory<
);
};
+ const signMessage = ({
+ message,
+ nonce,
+ recipient,
+ callbackUrl,
+ state,
+ }: SignMessageParams) => {
+ 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 href = new URL(params.walletUrl);
+ href.pathname = "sign-message";
+ href.searchParams.append("message", message);
+ href.searchParams.append("nonce", nonce.toString("base64"));
+ href.searchParams.append("recipient", recipient);
+ href.searchParams.append("callbackUrl", url);
+ if (state) {
+ href.searchParams.append("state", state);
+ }
+
+ window.location.replace(href.toString());
+ };
+
return {
async signIn({ contractId, methodNames, successUrl, failureUrl }) {
const existingAccounts = await getAccounts();
@@ -160,7 +249,7 @@ const MyNearWallet: WalletBehaviourFactory<
throw new Error(`Method not supported by ${metadata.name}`);
},
- async signMessage({ message, nonce, recipient, callbackUrl, state }) {
+ async signMessage(message) {
logger.log("sign message", { message });
if (id !== "my-near-wallet") {
@@ -168,27 +257,21 @@ const MyNearWallet: WalletBehaviourFactory<
`The signMessage method is not supported by ${metadata.name}`
);
}
+ signMessage(message);
- const locationUrl =
- typeof window !== "undefined" ? window.location.href : "";
-
- const url = callbackUrl || locationUrl;
+ return;
+ },
- if (!url) {
- throw new Error(`The callbackUrl is missing for ${metadata.name}`);
- }
+ async signInMessage(message) {
+ logger.log("signInMessage", { message });
- const href = new URL(params.walletUrl);
- href.pathname = "sign-message";
- href.searchParams.append("message", message);
- href.searchParams.append("nonce", nonce.toString("base64"));
- href.searchParams.append("recipient", recipient);
- href.searchParams.append("callbackUrl", url);
- if (state) {
- href.searchParams.append("state", state);
+ if (id !== "my-near-wallet") {
+ throw Error(
+ `The signInMessage method is not supported by ${metadata.name}`
+ );
}
- window.location.replace(href.toString());
+ signMessage(message);
return;
},
diff --git a/packages/near-mobile-wallet/src/lib/init.wallet.ts b/packages/near-mobile-wallet/src/lib/init.wallet.ts
index 17565d2de..9f4e170b2 100644
--- a/packages/near-mobile-wallet/src/lib/init.wallet.ts
+++ b/packages/near-mobile-wallet/src/lib/init.wallet.ts
@@ -1,6 +1,8 @@
import { NearMobileWallet } from "@peersyst/near-mobile-signer/dist/src/wallet/NearMobileWallet";
import type { NearMobileWalletInit } from "./near-mobile-wallet.types";
import type { Network } from "@peersyst/near-mobile-signer/dist/src/common/models";
+import type { Account } from "@near-wallet-selector/core";
+import { verifyMessageNEP413 } from "@near-wallet-selector/core";
export const initNearMobileWallet: NearMobileWalletInit = async (config) => {
const { store, options, logger, dAppMetadata } = config;
@@ -14,19 +16,28 @@ export const initNearMobileWallet: NearMobileWalletInit = async (config) => {
async function getAccounts() {
logger.log("[NearMobileWallet]:getAccounts");
const accountIds = await nearMobileWallet.getAccounts();
- const accounts = [];
-
- for (let i = 0; i < accountIds.length; i++) {
- accounts.push({
- accountId: accountIds[i],
- publicKey: (
- await nearMobileWallet.signer.getPublicKey(
- accountIds[i],
- options.network.networkId
- )
- ).toString(),
- });
+ const accounts: Array = [];
+ const { signedInMessageAccount } = store.getState();
+
+ if (accountIds.length > 0) {
+ for (let i = 0; i < accountIds.length; i++) {
+ accounts.push({
+ accountId: accountIds[i],
+ publicKey: (
+ await nearMobileWallet.signer.getPublicKey(
+ accountIds[i],
+ options.network.networkId
+ )
+ ).toString(),
+ });
+ }
+ return accounts;
}
+
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
+ }
+
return accounts;
}
@@ -45,7 +56,11 @@ export const initNearMobileWallet: NearMobileWalletInit = async (config) => {
async signOut() {
logger.log("[NearMobileWallet]: signOut");
- await nearMobileWallet.signOut();
+ const { signedInMessageAccount } = store.getState();
+
+ if (!signedInMessageAccount) {
+ await nearMobileWallet.signOut();
+ }
},
async getAccounts() {
@@ -87,8 +102,37 @@ export const initNearMobileWallet: NearMobileWalletInit = async (config) => {
};
},
+ async signInMessage(data) {
+ logger.log("[NearMobileWallet]: signInMessage", data);
+ const { recipient, nonce, ...rest } = data;
+ const signedMessage = await nearMobileWallet.signMessage({
+ ...rest,
+ receiver: recipient,
+ nonce: Array.from(nonce),
+ });
+
+ const message = { message: data.message, nonce, recipient };
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ signedMessage,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error("Failed to verify the message");
+ }
+
+ return signedMessage;
+ },
+
async signAndSendTransactions(data) {
logger.log("[NearMobileWallet]: signAndSendTransactions", data);
+
+ const { contract } = store.getState();
+ if (!contract) {
+ throw new Error("Wallet not signed in");
+ }
+
return await nearMobileWallet.signAndSendTransactions(data);
},
};
diff --git a/packages/near-snap/src/lib/selector.ts b/packages/near-snap/src/lib/selector.ts
index ba49ea261..464525141 100644
--- a/packages/near-snap/src/lib/selector.ts
+++ b/packages/near-snap/src/lib/selector.ts
@@ -4,6 +4,7 @@ import type {
WalletBehaviourFactory,
} from "@near-wallet-selector/core";
import { NearSnap, NearSnapAccount } from "@near-snap/sdk";
+import { verifyMessageNEP413 } from "@near-wallet-selector/core";
export const snap = new NearSnap();
@@ -67,6 +68,36 @@ export const initNearSnap: WalletBehaviourFactory = async (
return await account.signMessage({ message, nonce, recipient });
},
+ async signInMessage({ message, nonce, recipient }) {
+ let snapAccount: NearSnapAccount | null = null;
+ if (account == null) {
+ snapAccount = await NearSnapAccount.connect({
+ contractId: undefined,
+ methods: [],
+ network,
+ snap,
+ });
+ }
+
+ const currentAccount = account || snapAccount;
+ const signedMessage = await currentAccount!.signMessage({
+ message,
+ nonce,
+ recipient,
+ });
+ const isMessageVerified = await verifyMessageNEP413(
+ { message, nonce, recipient },
+ signedMessage,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
+ }
+
+ return signedMessage;
+ },
+
async verifyOwner() {
throw Error("NearSnap:verifyOwner is not released yet");
},
@@ -74,7 +105,9 @@ export const initNearSnap: WalletBehaviourFactory = async (
async signAndSendTransactions({ transactions }) {
logger.log("NearSnap:signAndSendTransactions", { transactions });
- if (account == null) {
+ const { contract } = store.getState();
+
+ if (account == null || !contract) {
throw new Error("Wallet not signed in");
}
diff --git a/packages/nightly/src/lib/nightly.ts b/packages/nightly/src/lib/nightly.ts
index 48d7e9056..11557244f 100644
--- a/packages/nightly/src/lib/nightly.ts
+++ b/packages/nightly/src/lib/nightly.ts
@@ -9,7 +9,7 @@ import type {
WalletEvents,
Account,
} from "@near-wallet-selector/core";
-import { waitFor } from "@near-wallet-selector/core";
+import { verifyMessageNEP413, waitFor } from "@near-wallet-selector/core";
import { signTransactions } from "@near-wallet-selector/wallet-utils";
import { isMobile } from "is-mobile";
import type { Signer } from "near-api-js";
@@ -147,12 +147,6 @@ const Nightly: WalletBehaviourFactory = async ({
return {
async signIn() {
- const existingAccounts = getAccounts();
-
- if (existingAccounts.length) {
- return existingAccounts;
- }
-
await _state.wallet.connect((newAcc) => {
if (!newAcc) {
emitter.emit("signedOut", null);
@@ -207,6 +201,28 @@ const Nightly: WalletBehaviourFactory = async ({
return signature;
},
+ async signInMessage(message) {
+ logger.log("Nightly:signInMessage", message);
+
+ if (!_state.wallet.isConnected) {
+ await _state.wallet.connect();
+ }
+
+ const signedMessage = await _state.wallet.signMessage(message);
+
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ signedMessage,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
+ }
+
+ return signedMessage;
+ },
+
async signAndSendTransaction({ signerId, receiverId, actions }) {
logger.log("signAndSendTransaction", { signerId, receiverId, actions });
diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts
index cd52cf6a1..3c9085ebe 100644
--- a/packages/sender/src/lib/sender.ts
+++ b/packages/sender/src/lib/sender.ts
@@ -9,7 +9,7 @@ import type {
Optional,
Account,
} from "@near-wallet-selector/core";
-import { waitFor } from "@near-wallet-selector/core";
+import { verifyMessageNEP413, waitFor } from "@near-wallet-selector/core";
import type { InjectedSender } from "./injected-sender";
import icon from "./icon";
@@ -105,8 +105,8 @@ const Sender: WalletBehaviourFactory = async ({
// Add extra wait to ensure Sender's sign in status is read from the
// browser extension background env.
// Check for isSignedIn() in only if selectedWalletId is set.
- const { selectedWalletId } = store.getState();
- if (selectedWalletId) {
+ const { selectedWalletId, signedInMessageAccount } = store.getState();
+ if (selectedWalletId && !signedInMessageAccount) {
await waitFor(() => !!_state.wallet?.isSignedIn(), {
timeout: 1000,
}).catch();
@@ -114,30 +114,34 @@ const Sender: WalletBehaviourFactory = async ({
const accountId = _state.wallet.getAccountId();
- if (!accountId) {
- return [];
- }
+ if (accountId) {
+ await waitFor(() => !!_state.wallet.account(), { timeout: 100 });
+
+ const account = _state.wallet.account();
- await waitFor(() => !!_state.wallet.account(), { timeout: 100 });
+ // When wallet is locked signer is empty an object {}.
+ if (!account!.connection.signer.getPublicKey) {
+ return [{ accountId, publicKey: undefined }];
+ }
- const account = _state.wallet.account();
+ const publicKey = await account!.connection.signer.getPublicKey(
+ account!.accountId,
+ options.network.networkId
+ );
- // When wallet is locked signer is empty an object {}.
- if (!account!.connection.signer.getPublicKey) {
- return [{ accountId, publicKey: undefined }];
+ return [
+ {
+ accountId,
+ publicKey: publicKey ? publicKey.toString() : undefined,
+ },
+ ];
}
- const publicKey = await account!.connection.signer.getPublicKey(
- account!.accountId,
- options.network.networkId
- );
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
+ }
- return [
- {
- accountId,
- publicKey: publicKey ? publicKey.toString() : undefined,
- },
- ];
+ return [];
};
const isValidActions = (
@@ -175,12 +179,6 @@ const Sender: WalletBehaviourFactory = async ({
return {
async signIn({ contractId, methodNames }) {
- const existingAccounts = await getAccounts();
-
- if (existingAccounts.length) {
- return existingAccounts;
- }
-
const { accessKey, error } = await _state.wallet.requestSignIn({
contractId,
methodNames,
@@ -266,6 +264,28 @@ const Sender: WalletBehaviourFactory = async ({
});
},
+ async signInMessage(message) {
+ const response = await _state.wallet.signMessage(message);
+ if (response.error) {
+ throw new Error(response.error);
+ }
+
+ if (!response?.response) {
+ throw new Error("Invalid response");
+ }
+
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ response.response,
+ options.network
+ );
+
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
+ }
+ return response.response;
+ },
+
async signAndSendTransaction({ signerId, receiverId, actions }) {
logger.log("signAndSendTransaction", { signerId, receiverId, actions });
diff --git a/packages/welldone-wallet/src/lib/welldone.ts b/packages/welldone-wallet/src/lib/welldone.ts
index e8831f112..a07a131ef 100644
--- a/packages/welldone-wallet/src/lib/welldone.ts
+++ b/packages/welldone-wallet/src/lib/welldone.ts
@@ -15,6 +15,7 @@ import type {
import {
isCurrentBrowserSupported,
serializeNep413,
+ verifyMessageNEP413,
waitFor,
} from "@near-wallet-selector/core";
import type {
@@ -108,14 +109,21 @@ const WelldoneWallet: WalletBehaviourFactory = async ({
};
const getAccounts = (): Array => {
- return _state.account
- ? [
- {
- accountId: _state.account.accountId,
- publicKey: _state.account.publicKey,
- },
- ]
- : [];
+ const { signedInMessageAccount } = store.getState();
+ if (_state.account) {
+ return [
+ {
+ accountId: _state.account.accountId,
+ publicKey: _state.account.publicKey,
+ },
+ ];
+ }
+
+ if (signedInMessageAccount) {
+ return [{ ...signedInMessageAccount }];
+ }
+
+ return [];
};
const signOut = async () => {
@@ -144,6 +152,36 @@ const WelldoneWallet: WalletBehaviourFactory = async ({
}
};
+ const signMessage = async (
+ message: SignMessageParams
+ ): Promise => {
+ if (!_state.wallet) {
+ throw new Error("Wallet is not installed");
+ }
+
+ const account = await _getAccounts();
+ const accountId = account[0];
+
+ if (!accountId) {
+ throw new Error("Failed to find account for signing");
+ }
+
+ const serializedTx = serializeNep413(message);
+ const signed = await _state.wallet.request("near", {
+ method: "dapp:signMessage",
+ params: ["0x" + serializedTx.toString("hex")],
+ });
+
+ return {
+ accountId,
+ publicKey: signed[0].publicKey,
+ signature: Buffer.from(signed[0].signature.substr(2), "hex").toString(
+ "base64"
+ ),
+ state: message?.state,
+ };
+ };
+
const signer: Signer = {
createKey: () => {
throw new Error("Not implemented");
@@ -219,12 +257,6 @@ const WelldoneWallet: WalletBehaviourFactory = async ({
return {
async signIn() {
- const existingAccounts = getAccounts();
-
- if (existingAccounts.length) {
- return existingAccounts;
- }
-
if (_state.account) {
await signOut();
}
@@ -301,38 +333,24 @@ const WelldoneWallet: WalletBehaviourFactory = async ({
};
},
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
async signMessage(message: SignMessageParams): Promise {
- if (!_state.wallet) {
- throw new Error("Wallet is not installed");
- }
-
- const account = await _getAccounts();
- const accountId = account[0];
-
- if (!accountId) {
- throw new Error("Failed to find account for signing");
- }
+ return await signMessage(message);
+ },
- const serializedTx = serializeNep413(message);
- const signed = await _state.wallet.request("near", {
- method: "dapp:signMessage",
- params: ["0x" + serializedTx.toString("hex")],
- });
+ async signInMessage(message: SignMessageParams): Promise {
+ const signedMessage = await signMessage(message);
- const result = {
- accountId,
- publicKey: signed[0].publicKey,
- signature: Buffer.from(signed[0].signature.substr(2), "hex").toString(
- "base64"
- ),
- };
+ const isMessageVerified = await verifyMessageNEP413(
+ message,
+ signedMessage,
+ options.network
+ );
- if (message.state) {
- return { ...result, state: message.state };
+ if (!isMessageVerified) {
+ throw new Error(`Failed to verify the message`);
}
- return result;
+ return signedMessage;
},
async signAndSendTransaction({ signerId, receiverId, actions }) {