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..3f4ee7367 100644 --- a/examples/angular/src/app/components/content/content.component.ts +++ b/examples/angular/src/app/components/content/content.component.ts @@ -111,6 +111,23 @@ export class ContentComponent implements OnInit, OnDestroy { alert("Switched account to " + nextAccountId); } + async onVerifyOwner() { + const wallet = await this.selector.wallet(); + try { + const owner = await wallet.verifyOwner({ + message: "test message for verification", + }); + + if (owner) { + alert(`Signature for verification: ${JSON.stringify(owner)}`); + } + } 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..ca24c5257 100644 --- a/examples/react/components/Content.tsx +++ b/examples/react/components/Content.tsx @@ -164,6 +164,23 @@ const Content: React.FC = () => { [selector, accountId] ); + const handleVerifyOwner = async () => { + const wallet = await selector.wallet(); + try { + const owner = await wallet.verifyOwner({ + message: "test message for verification", + }); + + if (owner) { + alert(`Signature for verification: ${JSON.stringify(owner)}`); + } + } catch (err) { + const message = + err instanceof Error ? err.message : "Something went wrong"; + alert(message); + } + }; + const handleSubmit = useCallback( async (e: SubmitEvent) => { e.preventDefault(); @@ -220,6 +237,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..549a77b1e 100644 --- a/packages/core/docs/api/wallet.md +++ b/packages/core/docs/api/wallet.md @@ -182,6 +182,35 @@ 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. + - `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/index.ts b/packages/core/src/index.ts index 5f94c63a7..6b6584a6c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -45,6 +45,8 @@ export type { BridgeWalletMetadata, BridgeWalletBehaviour, BridgeWallet, + VerifiedOwner, + VerifyOwnerParams, Account, Transaction, Action, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 435409d31..df06fa38f 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, @@ -29,6 +29,21 @@ export interface SignInParams { methodNames?: Array; } +export interface VerifyOwnerParams { + message: string; + callbackUrl?: string; + meta?: string; +} + +export interface VerifiedOwner { + accountId: string; + message: string; + blockId: string; + publicKey: string; + signature: string; + keyType: utils.key_pair.KeyType; +} + export interface SignAndSendTransactionParams { signerId?: string; receiverId?: string; @@ -43,6 +58,7 @@ 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..c967c32c1 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,12 @@ const Ledger: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message }) { + logger.log("Ledger:verifyOwner", { message }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + 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..c90d90622 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,44 @@ const MathWallet: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message }) { + logger.log("MathWallet:verifyOwner", { message }); + + const account = getActiveAccount(store.getState()); + + if (!account) { + throw new Error("No active account"); + } + + const accountId = account.accountId; + const pubKey = await _state.wallet.signer.getPublicKey(accountId); + const block = await provider.block({ finality: "final" }); + + const data = { + accountId, + message, + blockId: block.header.hash, + publicKey: Buffer.from(pubKey.data).toString("base64"), + keyType: pubKey.keyType, + }; + const encoded = JSON.stringify(data); + + // Note: Math Wallet currently hangs when calling signMessage. + throw new Error(`Method not supported by ${metadata.name}`); + + const signed = await _state.wallet.signer.signMessage( + new Uint8Array(Buffer.from(encoded)), + accountId, + options.network.networkId + ); + + return { + ...data, + signature: Buffer.from(signed.signature).toString("base64"), + keyType: signed.publicKey.keyType, + }; + }, + 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..27d642dbe 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 }) { + logger.log("MeteorWallet:verifyOwner", { message }); + + 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..87811d480 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,33 @@ const MyNearWallet: WalletBehaviourFactory< return getAccounts(); }, + async verifyOwner({ message, callbackUrl, meta }) { + logger.log("verifyOwner", { message }); + + 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..c79545d8a 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,12 @@ const NightlyConnect: WalletBehaviourFactory< return getAccounts().map(({ accountId }) => ({ accountId })); }, + async verifyOwner({ message }) { + logger.log("NightlyConnect:verifyOwner", { message }); + + 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..0bb7e92ff 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 }) { + logger.log("Nightly:verifyOwner", { message }); + + 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..6a04c153f 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: { @@ -114,6 +114,7 @@ export interface InjectedSender { callbacks: Record; getAccountId: () => string | null; getRpc: () => Promise; + account(): Account | null; requestSignIn: ( params: RequestSignInParams ) => Promise; diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index a64b0fd31..7160d5e02 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,52 @@ const Sender: WalletBehaviourFactory = async ({ return getAccounts(); }, + async verifyOwner({ message }) { + logger.log("Sender:verifyOwner", { message }); + + 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 = account.accountId; + const pubKey = await account.connection.signer.getPublicKey( + accountId, + networkId + ); + const block = await provider.block({ finality: "final" }); + + const data = { + accountId, + message, + blockId: block.header.hash, + publicKey: Buffer.from(pubKey.data).toString("base64"), + keyType: pubKey.keyType, + }; + const encoded = JSON.stringify(data); + + const signed = await account.connection.signer.signMessage( + new Uint8Array(Buffer.from(encoded)), + accountId, + networkId + ); + + return { + ...data, + signature: Buffer.from(signed.signature).toString("base64"), + keyType: signed.publicKey.keyType, + }; + }, + 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..229abd7c7 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 }) { + logger.log("WalletConnect:verifyOwner", { message }); + + throw new Error(`Method not supported by ${metadata.name}`); + }, + async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions });