@portal-hq/web 0.0.1-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,257 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { ProviderRpcError, RpcErrorCodes } from '@portal-hq/utils';
11
+ const passiveSignerMethods = [
12
+ 'eth_accounts',
13
+ 'eth_chainId',
14
+ 'eth_requestAccounts',
15
+ ];
16
+ const signerMethods = [
17
+ 'eth_accounts',
18
+ 'eth_chainId',
19
+ 'eth_requestAccounts',
20
+ 'eth_sendTransaction',
21
+ 'eth_sign',
22
+ 'eth_signTransaction',
23
+ 'eth_signTypedData_v3',
24
+ 'eth_signTypedData_v4',
25
+ 'personal_sign',
26
+ ];
27
+ class Provider {
28
+ constructor({ autoApprove = false, portal, }) {
29
+ this.autoApprove = autoApprove;
30
+ this.events = {};
31
+ this.portal = portal;
32
+ }
33
+ /**
34
+ * Invokes all registered event handlers with the data provided
35
+ * - If any `once` handlers exist, they are removed after all handlers are invoked
36
+ *
37
+ * @param event The name of the event to be handled
38
+ * @param data The data to be passed to registered event handlers
39
+ * @returns BaseProvider
40
+ */
41
+ emit(event, data) {
42
+ // Grab the registered event handlers if any are available
43
+ const handlers = this.events[event] || [];
44
+ // Execute every event handler
45
+ for (const registeredEventHandler of handlers) {
46
+ registeredEventHandler.handler(data);
47
+ }
48
+ // Remove any registered event handlers with the `once` flag
49
+ this.events[event] = handlers.filter((handler) => !handler.once);
50
+ return this;
51
+ }
52
+ /**
53
+ * Registers an event handler for the provided event
54
+ *
55
+ * @param event The event name to add a handler to
56
+ * @param callback The callback to be invoked when the event is emitted
57
+ * @returns BaseProvider
58
+ */
59
+ on(event, callback) {
60
+ // If no handlers are registered for this event, create an entry for the event
61
+ if (!this.events[event]) {
62
+ this.events[event] = [];
63
+ }
64
+ // Register event handler with the rudimentary event bus
65
+ if (typeof callback !== 'undefined') {
66
+ this.events[event].push({
67
+ handler: callback,
68
+ once: false,
69
+ });
70
+ }
71
+ return this;
72
+ }
73
+ removeEventListener(event, listenerToRemove) {
74
+ if (!this.events[event]) {
75
+ return;
76
+ }
77
+ if (!listenerToRemove) {
78
+ this.events[event] = [];
79
+ }
80
+ else {
81
+ const filterEventHandlers = (registeredEventHandler) => {
82
+ return registeredEventHandler.handler !== listenerToRemove;
83
+ };
84
+ this.events[event] = this.events[event].filter(filterEventHandlers);
85
+ }
86
+ }
87
+ /**
88
+ * Handles request routing in compliance with the EIP-1193 Ethereum Javascript Provider API
89
+ * - See here for more info: https://eips.ethereum.org/EIPS/eip-1193
90
+ *
91
+ * @param args The arguments of the request being made
92
+ * @returns Promise<any>
93
+ */
94
+ request({ method, params }) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ if (method === 'eth_chainId') {
97
+ return this.portal.chainId;
98
+ }
99
+ const isSignerMethod = signerMethods.includes(method);
100
+ let result;
101
+ if (!isSignerMethod && !method.startsWith('wallet_')) {
102
+ // Send to Gateway for RPC calls
103
+ const response = yield this.handleGatewayRequest({ method, params });
104
+ this.emit('portal_signatureReceived', {
105
+ method,
106
+ params,
107
+ signature: response,
108
+ });
109
+ if (response.error) {
110
+ throw new ProviderRpcError(response.error);
111
+ }
112
+ result = response.result;
113
+ }
114
+ else if (isSignerMethod) {
115
+ // Handle signing
116
+ const transactionHash = yield this.handleSigningRequest({
117
+ method,
118
+ params,
119
+ });
120
+ if (transactionHash) {
121
+ this.emit('portal_signatureReceived', {
122
+ method,
123
+ params,
124
+ signature: transactionHash,
125
+ });
126
+ result = transactionHash;
127
+ }
128
+ }
129
+ else {
130
+ // Unsupported method
131
+ throw new ProviderRpcError({
132
+ code: RpcErrorCodes.UnsupportedMethod,
133
+ data: {
134
+ method,
135
+ params,
136
+ },
137
+ });
138
+ }
139
+ return result;
140
+ });
141
+ }
142
+ /************************
143
+ * Private Methods
144
+ ************************/
145
+ /**
146
+ * Kicks off the approval flow for a given request
147
+ *
148
+ * @param args The arguments of the request being made
149
+ */
150
+ getApproval({ method, params, }) {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ // If autoApprove is enabled, just resolve to true
153
+ if (this.autoApprove) {
154
+ return true;
155
+ }
156
+ const signingHandlers = this.events['portal_signingRequested'];
157
+ if (!signingHandlers || signingHandlers.length === 0) {
158
+ throw new Error(`[PortalProvider] Auto-approve is disabled. Cannot perform signing requests without an event handler for the 'portal_signingRequested' event.`);
159
+ }
160
+ return new Promise((resolve) => {
161
+ // Remove already used listeners
162
+ this.removeEventListener('portal_signingApproved');
163
+ this.removeEventListener('portal_signingRejected');
164
+ const handleApproval = ({ method: approvedMethod, params: approvedParams }) => {
165
+ // Remove already used listeners
166
+ this.removeEventListener('portal_signingApproved');
167
+ this.removeEventListener('portal_signingRejected');
168
+ // First verify that this is the same signing request
169
+ if (method === approvedMethod &&
170
+ JSON.stringify(params) === JSON.stringify(approvedParams)) {
171
+ resolve(true);
172
+ }
173
+ };
174
+ const handleRejection = ({ method: rejectedMethod, params: rejectedParams }) => {
175
+ // Remove already used listeners
176
+ this.removeEventListener('portal_signingApproved');
177
+ this.removeEventListener('portal_signingRejected');
178
+ // First verify that this is the same signing request
179
+ if (method === rejectedMethod &&
180
+ JSON.stringify(params) === JSON.stringify(rejectedParams)) {
181
+ resolve(false);
182
+ }
183
+ };
184
+ // If the signing has been approved, resolve to true
185
+ this.on('portal_signingApproved', handleApproval);
186
+ // If the signing request has been rejected, resolve to false
187
+ this.on('portal_signingRejected', handleRejection);
188
+ // Tell any listening clients that signing has been requested
189
+ this.emit('portal_signingRequested', {
190
+ method,
191
+ params,
192
+ });
193
+ });
194
+ });
195
+ }
196
+ /**
197
+ * Sends the provided request payload along to the RPC HttpRequester
198
+ *
199
+ * @param args The arguments of the request being made
200
+ * @returns Promise<any>
201
+ */
202
+ handleGatewayRequest({ method, params, }) {
203
+ return __awaiter(this, void 0, void 0, function* () {
204
+ // Pass request off to the gateway
205
+ const result = yield fetch(this.portal.getRpcUrl(), {
206
+ body: JSON.stringify({
207
+ jsonrpc: '2.0',
208
+ id: this.portal.chainId,
209
+ method,
210
+ params,
211
+ }),
212
+ method: 'POST',
213
+ });
214
+ return result.json();
215
+ });
216
+ }
217
+ /**
218
+ * Sends the provided request payload along to the Signer
219
+ *
220
+ * @param args The arguments of the request being made
221
+ * @returns Promise<any>
222
+ */
223
+ handleSigningRequest({ method, params, }) {
224
+ return __awaiter(this, void 0, void 0, function* () {
225
+ const isApproved = passiveSignerMethods.includes(method)
226
+ ? true
227
+ : yield this.getApproval({ method, params });
228
+ if (!isApproved) {
229
+ console.info(`[PortalProvider] Request for signing method '${method}' could not be completed because it was not approved by the user.`);
230
+ return;
231
+ }
232
+ switch (method) {
233
+ case 'eth_chainId':
234
+ return `0x${this.portal.chainId.toString(16)}`;
235
+ case 'eth_accounts':
236
+ case 'eth_requestAccounts':
237
+ return [this.portal.address];
238
+ case 'eth_sendTransaction':
239
+ case 'eth_sign':
240
+ case 'eth_signTransaction':
241
+ case 'eth_signTypedData_v3':
242
+ case 'eth_signTypedData_v4':
243
+ case 'personal_sign':
244
+ const result = yield this.portal.mpc.sign({
245
+ host: this.portal.mpcHost,
246
+ method,
247
+ mpcVersion: this.portal.mpcVersion,
248
+ params,
249
+ });
250
+ return result;
251
+ default:
252
+ throw new Error('[PortalProvider] Method "' + method + '" not supported');
253
+ }
254
+ });
255
+ }
256
+ }
257
+ export default Provider;
@@ -0,0 +1,15 @@
1
+ class DefaultBackupStorage {
2
+ delete() {
3
+ throw new Error('[Portal] Storage method delete cannot be called directly. Please extend Storage.');
4
+ }
5
+ read() {
6
+ throw new Error('[Portal] Storage method read cannot be called directly. Please extend Storage.');
7
+ }
8
+ write(privateKey) {
9
+ throw new Error(`[Portal] Storage method write(${privateKey}) cannot be called directly. Please extend Storage.`);
10
+ }
11
+ validateOperations() {
12
+ throw new Error('[Portal] Storage method validateOperations cannot be called directly. Please extend Storage.');
13
+ }
14
+ }
15
+ export default DefaultBackupStorage;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@portal-hq/web",
3
+ "description": "Portal MPC Support for Web",
4
+ "author": "Portal Labs, Inc.",
5
+ "homepage": "https://portalhq.io/",
6
+ "version": "0.0.1-beta-1",
7
+ "license": "MIT",
8
+ "main": "lib/commonjs/index",
9
+ "module": "lib/esm/index",
10
+ "source": "src/index",
11
+ "types": "src/index",
12
+ "files": [
13
+ "src",
14
+ "lib",
15
+ "types.d.ts",
16
+ "!src/iframe"
17
+ ],
18
+ "scripts": {
19
+ "bundle": "sh ./scripts/version-iframe.sh && webpack && sh ./scripts/copy-static-files.sh && git stash",
20
+ "coverage": "jest --collect-coverage",
21
+ "prepare": "sh ./scripts/version-mpc.sh && yarn prepare:cjs && yarn prepare:esm && rm -rf lib/esm/iframe && rm -rf lib/commonjs/iframe",
22
+ "prepare:cjs": "tsc --outDir lib/commonjs --module commonjs",
23
+ "prepare:esm": "tsc --outDir lib/esm --module es2015 --target es2015",
24
+ "test": "jest"
25
+ },
26
+ "dependencies": {
27
+ "@portal-hq/utils": "^1.0.2"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/core": "^7.22.5",
31
+ "@babel/preset-env": "^7.22.5",
32
+ "@babel/preset-typescript": "^7.18.6",
33
+ "@types/jest": "^29.2.0",
34
+ "@types/react": "*",
35
+ "@types/react-native": "^0.70.5",
36
+ "babel-loader": "^9.1.2",
37
+ "jest": "^29.2.1",
38
+ "jest-environment-jsdom": "^29.2.2",
39
+ "terser-webpack-plugin": "^5.3.9",
40
+ "ts-jest": "^29.0.3",
41
+ "ts-loader": "^9.4.3",
42
+ "typescript": "^4.8.4",
43
+ "webpack": "^5.87.0",
44
+ "webpack-cli": "^5.1.4"
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,232 @@
1
+ import type { BackupOptions, EthereumTransaction, GatewayLike, PortalOptions, TypedData } from '../types'
2
+ import Mpc from './mpc'
3
+ import Provider from './provider'
4
+ import DefaultBackupStorage from './storage'
5
+
6
+ class Portal {
7
+ public address?: string
8
+ public chainId: number
9
+ public mpc: Mpc
10
+ public mpcHost: string
11
+ public mpcVersion: string
12
+ public provider: Provider
13
+ public ready: boolean = false
14
+
15
+ private apiHost: string
16
+ private apiKey: string
17
+ private autoApprove: boolean
18
+ private backup: BackupOptions
19
+ private gatewayConfig: GatewayLike
20
+ private readyCallbacks: (() => any | Promise<any>)[] = []
21
+ private rpcUrl: string
22
+ private webHost: string
23
+
24
+ constructor({
25
+ // Required
26
+ apiKey,
27
+ gatewayConfig,
28
+
29
+ // Optional
30
+ apiHost = 'api.portalhq.io',
31
+ autoApprove = false,
32
+ backup = { default: new DefaultBackupStorage() },
33
+ chainId = 1,
34
+ mpcHost = 'mpc.portalhq.io',
35
+ mpcVersion = 'v4',
36
+ webHost = 'web.portalhq.io',
37
+ }: PortalOptions) {
38
+ this.apiHost = apiHost
39
+ this.apiKey = apiKey
40
+ this.autoApprove = autoApprove
41
+ this.backup = backup
42
+ this.chainId = chainId
43
+ this.gatewayConfig = gatewayConfig
44
+ this.mpcHost = mpcHost
45
+ this.mpcVersion = mpcVersion
46
+ this.rpcUrl = this.getRpcUrl()
47
+ this.webHost = webHost
48
+
49
+ console.log(`Backup:`, this.backup)
50
+
51
+ // Portal Instances
52
+ this.mpc = new Mpc({
53
+ apiHost: this.apiHost,
54
+ apiKey: this.apiKey,
55
+ autoApprove: this.autoApprove,
56
+ chainId: this.chainId,
57
+ mpcHost: this.mpcHost,
58
+ rpcUrl: this.rpcUrl,
59
+ webHost: this.webHost,
60
+ portal: this,
61
+ })
62
+
63
+ this.provider = new Provider({
64
+ autoApprove: this.autoApprove,
65
+ mpcHost: this.mpcHost,
66
+ mpcVersion: this.mpcVersion,
67
+ portal: this,
68
+ })
69
+ }
70
+
71
+ /*****************************
72
+ * Initialization Methods
73
+ *****************************/
74
+
75
+ public onReady(callback: () => any | Promise<any>) {
76
+ if (this.ready) {
77
+ callback()
78
+ } else {
79
+ this.readyCallbacks.push(callback)
80
+ }
81
+ }
82
+
83
+ public triggerReady() {
84
+ if (this.ready && this.readyCallbacks.length > 0) {
85
+ this.readyCallbacks.forEach((callback) => {
86
+ callback()
87
+ })
88
+
89
+ this.readyCallbacks = []
90
+ }
91
+ }
92
+
93
+ /*****************************
94
+ * Wallet Methods
95
+ *****************************/
96
+
97
+ public async createWallet(): Promise<string> {
98
+ const address = await this.mpc.generate({
99
+ host: this.mpcHost,
100
+ mpcVersion: this.mpcVersion,
101
+ })
102
+
103
+ this.address = address
104
+
105
+ return address
106
+ }
107
+
108
+ public async backupWallet(): Promise<string> {
109
+ await this.mpc.backup({
110
+ host: this.mpcHost,
111
+ mpcVersion: this.mpcVersion,
112
+ })
113
+
114
+ return ''
115
+ }
116
+
117
+ public async recoverWallet(): Promise<boolean> {
118
+ await this.mpc.recover({
119
+ host: this.mpcHost,
120
+ mpcVersion: this.mpcVersion,
121
+ })
122
+
123
+ return true
124
+ }
125
+
126
+ /****************************
127
+ * Provider Methods
128
+ ****************************/
129
+
130
+ public async ethSendTransaction(transaction: EthereumTransaction): Promise<string> {
131
+ return this.provider.request({
132
+ method: 'eth_sendTransaction',
133
+ params: transaction,
134
+ })
135
+ }
136
+
137
+ public async ethSign(message: string): Promise<string> {
138
+ return this.provider.request({
139
+ method: 'eth_sign',
140
+ params: [this.address, this.stringToHex(message)],
141
+ })
142
+ }
143
+
144
+ public async ethSignTransaction(transaction: EthereumTransaction): Promise<string> {
145
+ return this.provider.request({
146
+ method: 'eth_signTransaction',
147
+ params: transaction,
148
+ })
149
+ }
150
+
151
+ public async ethSignTypedData(data: TypedData): Promise<string> {
152
+ return this.provider.request({
153
+ method: 'eth_signTypedData',
154
+ params: [this.address, data],
155
+ })
156
+ }
157
+
158
+ public async ethSignTypedDataV3(data: TypedData): Promise<string> {
159
+ return this.provider.request({
160
+ method: 'eth_signTypedData_v3',
161
+ params: [this.address, data],
162
+ })
163
+
164
+ }
165
+
166
+ public async ethSignTypedDataV4(data: TypedData): Promise<string> {
167
+ return this.provider.request({
168
+ method: 'eth_signTypedData_v4',
169
+ params: [this.address, data],
170
+ })
171
+ }
172
+
173
+ public async personalSign(message: string): Promise<string> {
174
+ return this.provider.request({
175
+ method: 'personal_sign',
176
+ params: [this.stringToHex(message), this.address],
177
+ })
178
+ }
179
+
180
+ /****************************
181
+ * RPC Methods
182
+ ****************************/
183
+
184
+ public getRpcUrl() {
185
+ if (typeof this.gatewayConfig === 'string') {
186
+ // If the gatewayConfig is just a static URL, return that
187
+ return this.gatewayConfig
188
+ } else if (
189
+ typeof this.gatewayConfig === 'object' &&
190
+ !this.gatewayConfig.hasOwnProperty(this.chainId)
191
+ ) {
192
+ // If there's no explicit mapping for the current chainId, error out
193
+ throw new Error(
194
+ `[PortalProvider] No RPC endpoint configured for chainId: ${this.chainId}`,
195
+ )
196
+ }
197
+
198
+ // Get the entry for the current chainId from the gatewayConfig
199
+ const config = this.gatewayConfig[this.chainId]
200
+
201
+ if (typeof config === 'string') {
202
+ return config
203
+ }
204
+
205
+ // If we got this far, there's no way to support the chain with the current config
206
+ throw new Error(
207
+ `[PortalProvider] Could not find a valid gatewayConfig entry for chainId: ${this.chainId}`,
208
+ )
209
+ }
210
+
211
+ /**************************
212
+ * RPC Encoding Methods
213
+ **************************/
214
+
215
+ private stringToHex(str: string): string {
216
+ if (str.startsWith('0x')) {
217
+ return str
218
+ }
219
+
220
+ let hex = '0x';
221
+
222
+ for (let i = 0; i < str.length; i++) {
223
+ const charCode = str.charCodeAt(i);
224
+ const hexValue = charCode.toString(16);
225
+ hex += hexValue.padStart(2, '0'); // Ensure two-digit hex value
226
+ }
227
+
228
+ return hex;
229
+ }
230
+ }
231
+
232
+ export default Portal