diff --git a/package-lock.json b/package-lock.json index 85671783c..f83608bd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27492,6 +27492,7 @@ "@walletconnect/types": "2.10.6", "@walletconnect/utils": "2.10.6", "events": "^3.3.0", + "isomorphic-unfetch": "3.1.0", "lodash.isequal": "4.5.0", "uint8arrays": "^3.1.0" }, @@ -33591,6 +33592,7 @@ "@walletconnect/types": "2.10.6", "@walletconnect/utils": "2.10.6", "events": "^3.3.0", + "isomorphic-unfetch": "3.1.0", "lodash.isequal": "4.5.0", "node-fetch": "^3.3.0", "uint8arrays": "^3.1.0" diff --git a/packages/core/package.json b/packages/core/package.json index f04269669..67c93ee2e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,8 @@ "@walletconnect/utils": "2.10.6", "events": "^3.3.0", "lodash.isequal": "4.5.0", - "uint8arrays": "^3.1.0" + "uint8arrays": "^3.1.0", + "isomorphic-unfetch": "3.1.0" }, "devDependencies": { "@types/lodash.isequal": "4.5.6", diff --git a/packages/core/src/constants/echo.ts b/packages/core/src/constants/echo.ts new file mode 100644 index 000000000..b845ef8f8 --- /dev/null +++ b/packages/core/src/constants/echo.ts @@ -0,0 +1,3 @@ +export const ECHO_CONTEXT = "echo"; + +export const ECHO_URL = "https://echo.walletconnect.com"; diff --git a/packages/core/src/constants/index.ts b/packages/core/src/constants/index.ts index e374f4e86..937b946ce 100644 --- a/packages/core/src/constants/index.ts +++ b/packages/core/src/constants/index.ts @@ -10,3 +10,4 @@ export * from "./pairing"; export * from "./history"; export * from "./expirer"; export * from "./verify"; +export * from "./echo"; diff --git a/packages/core/src/controllers/echo.ts b/packages/core/src/controllers/echo.ts new file mode 100644 index 000000000..ccf86f7b3 --- /dev/null +++ b/packages/core/src/controllers/echo.ts @@ -0,0 +1,31 @@ +import { generateChildLogger, Logger } from "@walletconnect/logger"; +import { IEchoClient } from "@walletconnect/types"; +import { ECHO_CONTEXT, ECHO_URL } from "../constants"; +import fetch from "isomorphic-unfetch"; + +export class EchoClient extends IEchoClient { + public readonly context = ECHO_CONTEXT; + constructor(public projectId: string, public logger: Logger) { + super(projectId, logger); + this.logger = generateChildLogger(logger, this.context); + } + + public registerDeviceToken: IEchoClient["registerDeviceToken"] = async (params) => { + const { clientId, token, notificationType, enableEncrypted = false } = params; + + const echoUrl = `${ECHO_URL}/${this.projectId}/clients`; + + await fetch(echoUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: clientId, + type: notificationType, + token, + always_raw: enableEncrypted, + }), + }); + }; +} diff --git a/packages/core/src/controllers/index.ts b/packages/core/src/controllers/index.ts index c9abcedc8..57ac377a4 100644 --- a/packages/core/src/controllers/index.ts +++ b/packages/core/src/controllers/index.ts @@ -8,3 +8,4 @@ export * from "./pairing"; export * from "./history"; export * from "./expirer"; export * from "./verify"; +export * from "./echo"; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index ec1b10f86..fc605fdf8 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -10,7 +10,15 @@ import { } from "@walletconnect/logger"; import { CoreTypes, ICore } from "@walletconnect/types"; -import { Crypto, Relayer, Pairing, JsonRpcHistory, Expirer, Verify } from "./controllers"; +import { + Crypto, + Relayer, + Pairing, + JsonRpcHistory, + Expirer, + Verify, + EchoClient, +} from "./controllers"; import { CORE_CONTEXT, CORE_DEFAULT, @@ -39,6 +47,7 @@ export class Core extends ICore { public expirer: ICore["expirer"]; public pairing: ICore["pairing"]; public verify: ICore["verify"]; + public echoClient: ICore["echoClient"]; private initialized = false; @@ -77,6 +86,7 @@ export class Core extends ICore { }); this.pairing = new Pairing(this, this.logger); this.verify = new Verify(this.projectId || "", this.logger); + this.echoClient = new EchoClient(this.projectId || "", this.logger); } get context() { diff --git a/packages/sign-client/src/index.ts b/packages/sign-client/src/index.ts index 09f073828..eafc655b9 100644 --- a/packages/sign-client/src/index.ts +++ b/packages/sign-client/src/index.ts @@ -1,6 +1,7 @@ import { SignClient as Client } from "./client"; - +import { Session } from "./controllers/session"; export * from "./constants"; +export const SessionStore = Session; export const SignClient = Client; export default Client; diff --git a/packages/types/src/core/core.ts b/packages/types/src/core/core.ts index fb25b0efb..f92d61918 100644 --- a/packages/types/src/core/core.ts +++ b/packages/types/src/core/core.ts @@ -10,6 +10,7 @@ import { IExpirer } from "./expirer"; import { IPairing } from "./pairing"; import { Logger } from "@walletconnect/logger"; import { IVerify } from "./verify"; +import { IEchoClient } from "./echo"; export declare namespace CoreTypes { interface Options { projectId?: string; @@ -54,6 +55,7 @@ export abstract class ICore extends IEvents { public abstract expirer: IExpirer; public abstract pairing: IPairing; public abstract verify: IVerify; + public abstract echoClient: IEchoClient; constructor(public opts?: CoreTypes.Options) { super(); diff --git a/packages/types/src/core/echo.ts b/packages/types/src/core/echo.ts new file mode 100644 index 000000000..bd94972ea --- /dev/null +++ b/packages/types/src/core/echo.ts @@ -0,0 +1,18 @@ +import { Logger } from "@walletconnect/logger"; + +export declare namespace EchoClientTypes { + type RegisterDeviceTokenParams = { + clientId: string; + token: string; + notificationType: "fcm" | "apns" | "apns-sandbox" | "noop"; + enableEncrypted?: boolean; + }; +} +export abstract class IEchoClient { + public abstract readonly context: string; + constructor(public projectId: string, public logger: Logger) {} + + public abstract registerDeviceToken( + params: EchoClientTypes.RegisterDeviceTokenParams, + ): Promise; +} diff --git a/packages/types/src/core/index.ts b/packages/types/src/core/index.ts index d9b28a6ca..08aa7e323 100644 --- a/packages/types/src/core/index.ts +++ b/packages/types/src/core/index.ts @@ -10,3 +10,4 @@ export * from "./keychain"; export * from "./expirer"; export * from "./pairing"; export * from "./verify"; +export * from "./echo"; diff --git a/packages/web3wallet/src/client.ts b/packages/web3wallet/src/client.ts index 87142748c..7991bf03f 100644 --- a/packages/web3wallet/src/client.ts +++ b/packages/web3wallet/src/client.ts @@ -2,6 +2,7 @@ import EventEmitter from "events"; import { CLIENT_CONTEXT } from "./constants"; import { Engine } from "./controllers"; import { IWeb3Wallet, Web3WalletTypes } from "./types"; +import { Notifications } from "./utils"; export class Web3Wallet extends IWeb3Wallet { public name: IWeb3Wallet["name"]; @@ -10,6 +11,7 @@ export class Web3Wallet extends IWeb3Wallet { public events: IWeb3Wallet["events"] = new EventEmitter(); public engine: IWeb3Wallet["engine"]; public metadata: IWeb3Wallet["metadata"]; + public static notifications: Web3WalletTypes.INotifications = Notifications; static async init(opts: Web3WalletTypes.Options) { const client = new Web3Wallet(opts); @@ -173,6 +175,15 @@ export class Web3Wallet extends IWeb3Wallet { } }; + public registerDeviceToken: IWeb3Wallet["registerDeviceToken"] = (params) => { + try { + return this.engine.registerDeviceToken(params); + } catch (error: any) { + this.logger.error(error.message); + throw error; + } + }; + // ---------- Private ----------------------------------------------- // private async initialize() { diff --git a/packages/web3wallet/src/controllers/engine.ts b/packages/web3wallet/src/controllers/engine.ts index f13a05c91..cbcbdcfbd 100644 --- a/packages/web3wallet/src/controllers/engine.ts +++ b/packages/web3wallet/src/controllers/engine.ts @@ -98,6 +98,11 @@ export class Engine extends IWeb3WalletEngine { return this.authClient.formatMessage(params, iss); }; + // Push // + public registerDeviceToken: IWeb3WalletEngine["registerDeviceToken"] = (params) => { + return this.client.core.echoClient.registerDeviceToken(params); + }; + // ---------- Private ----------------------------------------------- // private onSessionRequest = (event: Web3WalletTypes.SessionRequest) => { diff --git a/packages/web3wallet/src/types/client.ts b/packages/web3wallet/src/types/client.ts index 0e13c00ce..48865f250 100644 --- a/packages/web3wallet/src/types/client.ts +++ b/packages/web3wallet/src/types/client.ts @@ -1,8 +1,9 @@ import EventEmmiter, { EventEmitter } from "events"; -import { ICore, ProposalTypes, Verify } from "@walletconnect/types"; +import { ICore, CoreTypes, ProposalTypes, Verify } from "@walletconnect/types"; import { AuthClientTypes } from "@walletconnect/auth-client"; import { IWeb3WalletEngine } from "./engine"; import { Logger } from "@walletconnect/logger"; +import { JsonRpcPayload } from "@walletconnect/jsonrpc-utils"; export declare namespace Web3WalletTypes { type Event = "session_proposal" | "session_request" | "session_delete" | "auth_request"; @@ -41,7 +42,21 @@ export declare namespace Web3WalletTypes { name?: string; } - type Metadata = AuthClientTypes.Metadata; + type Metadata = CoreTypes.Metadata; + + interface INotifications { + decryptMessage: (params: { + topic: string; + encryptedMessage: string; + storageOptions?: CoreTypes.Options["storageOptions"]; + storage?: CoreTypes.Options["storage"]; + }) => Promise; + getMetadata: (params: { + topic: string; + storageOptions?: CoreTypes.Options["storageOptions"]; + storage?: CoreTypes.Options["storage"]; + }) => Promise; + } } export abstract class IWeb3WalletEvents extends EventEmmiter { @@ -104,6 +119,8 @@ export abstract class IWeb3Wallet { public abstract respondAuthRequest: IWeb3WalletEngine["respondAuthRequest"]; public abstract getPendingAuthRequests: IWeb3WalletEngine["getPendingAuthRequests"]; public abstract formatMessage: IWeb3WalletEngine["formatMessage"]; + // push + public abstract registerDeviceToken: IWeb3WalletEngine["registerDeviceToken"]; // ---------- Event Handlers ----------------------------------------------- // public abstract on: ( diff --git a/packages/web3wallet/src/types/engine.ts b/packages/web3wallet/src/types/engine.ts index 47b4ae6cb..65f6760e6 100644 --- a/packages/web3wallet/src/types/engine.ts +++ b/packages/web3wallet/src/types/engine.ts @@ -5,6 +5,7 @@ import { PendingRequestTypes, ProposalTypes, SessionTypes, + EchoClientTypes, } from "@walletconnect/types"; import { IWeb3Wallet } from "./client"; @@ -83,4 +84,9 @@ export abstract class IWeb3WalletEngine { // format payload to message string public abstract formatMessage(payload: AuthEngineTypes.CacaoRequestPayload, iss: string): string; + + // ---------- Push ------------------------------------------------- // + public abstract registerDeviceToken( + params: EchoClientTypes.RegisterDeviceTokenParams, + ): Promise; } diff --git a/packages/web3wallet/src/utils/index.ts b/packages/web3wallet/src/utils/index.ts new file mode 100644 index 000000000..adcb2c9f5 --- /dev/null +++ b/packages/web3wallet/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./notifications"; diff --git a/packages/web3wallet/src/utils/notifications.ts b/packages/web3wallet/src/utils/notifications.ts new file mode 100644 index 000000000..4548c79a5 --- /dev/null +++ b/packages/web3wallet/src/utils/notifications.ts @@ -0,0 +1,34 @@ +import { Core } from "@walletconnect/core"; +import { Web3WalletTypes } from "../types"; +import { SessionStore } from "@walletconnect/sign-client"; + +export const Notifications: Web3WalletTypes.INotifications = { + decryptMessage: async (params) => { + const instance = { + core: new Core({ + storageOptions: params.storageOptions, + storage: params.storage, + }), + } as any; + await instance.core.crypto.init(); + const decoded = instance.core.crypto.decode(params.topic, params.encryptedMessage); + instance.core = null; + return decoded; + }, + getMetadata: async (params) => { + const instances = { + core: new Core({ + storageOptions: params.storageOptions, + storage: params.storage, + }), + sessionStore: null, + } as any; + instances.sessionStore = new SessionStore(instances.core, instances.core.logger); + await instances.sessionStore.init(); + const session = instances.sessionStore.get(params.topic); + const metadata = session?.peer.metadata; + instances.core = null; + instances.sessionStore = null; + return metadata; + }, +}; diff --git a/packages/web3wallet/test/sign.spec.ts b/packages/web3wallet/test/sign.spec.ts index 8a4868fde..8194e388c 100644 --- a/packages/web3wallet/test/sign.spec.ts +++ b/packages/web3wallet/test/sign.spec.ts @@ -1,7 +1,11 @@ -import { Core } from "@walletconnect/core"; -import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils"; +import { Core, RELAYER_EVENTS } from "@walletconnect/core"; +import { + JsonRpcPayload, + formatJsonRpcResult, + isJsonRpcRequest, +} from "@walletconnect/jsonrpc-utils"; import { SignClient } from "@walletconnect/sign-client"; -import { ICore, ISignClient, SessionTypes } from "@walletconnect/types"; +import { CoreTypes, ICore, ISignClient, SessionTypes } from "@walletconnect/types"; import { getSdkError } from "@walletconnect/utils"; import { Wallet as CryptoWallet } from "@ethersproject/wallet"; @@ -445,4 +449,237 @@ describe("Sign Integration", () => { }), ]); }); + + describe("Decrypted notifications", () => { + it("should get session metadata", async () => { + const dappMetadata: CoreTypes.Metadata = { + name: "Test Dapp", + description: "Test Dapp Description", + url: "https://walletconnect.com", + icons: ["https://walletconnect.com/walletconnect-logo.png"], + }; + const dappTable = "./test/tmp/dapp"; + const walletTable = "./test/tmp/wallet"; + const dapp = await SignClient.init({ + ...TEST_CORE_OPTIONS, + name: "Dapp", + metadata: dappMetadata, + storageOptions: { + database: dappTable, + }, + }); + const wallet = await Web3Wallet.init({ + core: new Core({ + ...TEST_CORE_OPTIONS, + storageOptions: { database: walletTable }, + }), + name: "wallet", + metadata: {} as any, + }); + + const { uri: uriString, approval } = await dapp.connect({}); + let session: SessionTypes.Struct; + await Promise.all([ + new Promise((resolve) => { + wallet.on("session_proposal", async (sessionProposal) => { + const { id, params, verifyContext } = sessionProposal; + expect(verifyContext.verified.validation).to.eq("UNKNOWN"); + expect(verifyContext.verified.isScam).to.eq(undefined); + session = await wallet.approveSession({ + id, + namespaces: TEST_NAMESPACES, + }); + resolve(session); + }); + }), + new Promise(async (resolve) => { + resolve(await approval()); + }), + wallet.pair({ uri: uriString! }), + ]); + + const metadata = await Web3Wallet.notifications.getMetadata({ + topic: session?.topic, + storageOptions: { database: walletTable }, + }); + + expect(metadata).to.be.exist; + expect(metadata).to.be.a("object"); + expect(metadata).to.toMatchObject(dappMetadata); + }); + + it("should decrypt payload with pairing topic", async () => { + const dappMetadata: CoreTypes.Metadata = { + name: "Test Dapp", + description: "Test Dapp Description", + url: "https://walletconnect.com", + icons: ["https://walletconnect.com/walletconnect-logo.png"], + }; + const dappTable = "./test/tmp/dapp"; + const walletTable = "./test/tmp/wallet"; + const dapp = await SignClient.init({ + ...TEST_CORE_OPTIONS, + name: "Dapp", + metadata: dappMetadata, + storageOptions: { + database: dappTable, + }, + }); + const wallet = await Web3Wallet.init({ + core: new Core({ + ...TEST_CORE_OPTIONS, + storageOptions: { database: walletTable }, + }), + name: "wallet", + metadata: {} as any, + }); + + const { uri: uriString = "", approval } = await dapp.connect({}); + let encryptedMessage = ""; + let decryptedMessage: JsonRpcPayload = {} as any; + let pairingTopic = ""; + await Promise.all([ + new Promise((resolve) => { + wallet.core.relayer.on(RELAYER_EVENTS.message, async (payload) => { + const { topic, message } = payload; + const decrypted = await wallet.core.crypto.decode(topic, message); + expect(decrypted).to.be.exist; + if (decrypted?.method === "wc_sessionPropose" && isJsonRpcRequest(decrypted)) { + encryptedMessage = message; + decryptedMessage = decrypted; + pairingTopic = topic; + resolve(); + } + }); + }), + new Promise((resolve) => { + wallet.on("session_proposal", async (sessionProposal) => { + const { id, params, verifyContext } = sessionProposal; + expect(verifyContext.verified.validation).to.eq("UNKNOWN"); + expect(verifyContext.verified.isScam).to.eq(undefined); + await wallet.approveSession({ + id, + namespaces: TEST_NAMESPACES, + }); + resolve(); + }); + }), + new Promise(async (resolve) => { + resolve(await approval()); + }), + wallet.pair({ uri: uriString }), + ]); + + const decrypted = await Web3Wallet.notifications.decryptMessage({ + topic: pairingTopic, + encryptedMessage, + storageOptions: { database: walletTable }, + }); + expect(decrypted).to.be.exist; + expect(decrypted).to.be.a("object"); + expect(decrypted).to.toMatchObject(decryptedMessage); + }); + it("should decrypt payload with session topic", async () => { + const dappMetadata: CoreTypes.Metadata = { + name: "Test Dapp", + description: "Test Dapp Description", + url: "https://walletconnect.com", + icons: ["https://walletconnect.com/walletconnect-logo.png"], + }; + const dappTable = "./test/tmp/dapp"; + const walletTable = "./test/tmp/wallet"; + const dapp = await SignClient.init({ + ...TEST_CORE_OPTIONS, + name: "Dapp", + metadata: dappMetadata, + storageOptions: { + database: dappTable, + }, + }); + const wallet = await Web3Wallet.init({ + core: new Core({ + ...TEST_CORE_OPTIONS, + storageOptions: { database: walletTable }, + }), + name: "wallet", + metadata: {} as any, + }); + + const { uri: uriString = "", approval } = await dapp.connect({}); + + let session: SessionTypes.Struct = {} as any; + // pair and approve session + await Promise.all([ + new Promise((resolve) => { + wallet.on("session_proposal", async (sessionProposal) => { + const { id, params, verifyContext } = sessionProposal; + expect(verifyContext.verified.validation).to.eq("UNKNOWN"); + expect(verifyContext.verified.isScam).to.eq(undefined); + session = await wallet.approveSession({ + id, + namespaces: TEST_NAMESPACES, + }); + resolve(); + }); + }), + new Promise(async (resolve) => { + resolve(await approval()); + }), + wallet.pair({ uri: uriString }), + ]); + + let encryptedMessage = ""; + let decryptedMessage: JsonRpcPayload = {} as any; + await Promise.all([ + new Promise((resolve) => { + wallet.core.relayer.on(RELAYER_EVENTS.message, async (payload) => { + const { topic, message } = payload; + const decrypted = await wallet.core.crypto.decode(topic, message); + expect(decrypted).to.be.exist; + if (decrypted?.method === "wc_sessionRequest" && isJsonRpcRequest(decrypted)) { + encryptedMessage = message; + decryptedMessage = decrypted; + resolve(); + } + }); + }), + new Promise((resolve) => { + wallet.on("session_request", async (payload) => { + const { id, params, topic, verifyContext } = payload; + await wallet.respondSessionRequest({ + topic, + response: formatJsonRpcResult(id, "0x"), + }); + resolve(); + }); + }), + dapp.request({ + topic: session.topic, + request: { + method: "eth_signTransaction", + params: [ + { + from: cryptoWallet.address, + to: cryptoWallet.address, + data: "0x", + nonce: "0x01", + gasPrice: "0x020a7ac094", + gasLimit: "0x5208", + value: "0x00", + }, + ], + }, + chainId: TEST_ETHEREUM_CHAIN, + }), + ]); + const decrypted = await Web3Wallet.notifications.decryptMessage({ + topic: session.topic, + encryptedMessage, + storageOptions: { database: walletTable }, + }); + expect(decrypted).to.be.exist; + expect(decrypted).to.be.a("object"); + expect(decrypted).to.toMatchObject(decryptedMessage); + }); + }); });