@portal-hq/web 3.14.0 → 3.15.0-alpha.2
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.
- package/lib/commonjs/index.js +4 -0
- package/lib/commonjs/mpc/index.js +50 -1
- package/lib/commonjs/mpc/index.test.js +48 -1
- package/lib/commonjs/provider/index.js +3 -0
- package/lib/commonjs/provider/index.test.js +110 -0
- package/lib/esm/index.js +4 -0
- package/lib/esm/mpc/index.js +50 -1
- package/lib/esm/mpc/index.test.js +48 -1
- package/lib/esm/provider/index.js +3 -0
- package/lib/esm/provider/index.test.js +110 -0
- package/package.json +3 -2
- package/src/index.ts +9 -0
- package/src/mpc/index.test.ts +70 -1
- package/src/mpc/index.ts +59 -1
- package/src/provider/index.test.ts +137 -0
- package/src/provider/index.ts +3 -0
- package/src/shared/types/common.ts +7 -1
- package/types.d.ts +5 -0
package/lib/commonjs/index.js
CHANGED
|
@@ -316,6 +316,9 @@ class Portal {
|
|
|
316
316
|
yield this.storedClientBackupShare(true, BackupMethods.passkey);
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
+
configureFirebaseStorage(options) {
|
|
320
|
+
this.mpc.configureFirebaseStorage(options);
|
|
321
|
+
}
|
|
319
322
|
backupWallet(backupMethod, progress = () => {
|
|
320
323
|
// Noop
|
|
321
324
|
}, backupConfigs = {}) {
|
|
@@ -1144,6 +1147,7 @@ var BackupMethods;
|
|
|
1144
1147
|
BackupMethods["password"] = "PASSWORD";
|
|
1145
1148
|
BackupMethods["passkey"] = "PASSKEY";
|
|
1146
1149
|
BackupMethods["custom"] = "CUSTOM";
|
|
1150
|
+
BackupMethods["firebase"] = "FIREBASE";
|
|
1147
1151
|
BackupMethods["unknown"] = "UNKNOWN";
|
|
1148
1152
|
})(BackupMethods = exports.BackupMethods || (exports.BackupMethods = {}));
|
|
1149
1153
|
var GetTransactionsOrder;
|
|
@@ -14,7 +14,7 @@ const errors_1 = require("./errors");
|
|
|
14
14
|
const logger_1 = require("../logger");
|
|
15
15
|
const index_1 = require("../index");
|
|
16
16
|
const trace_1 = require("../shared/trace");
|
|
17
|
-
const WEB_SDK_VERSION = '3.
|
|
17
|
+
const WEB_SDK_VERSION = '3.15.0-alpha.2';
|
|
18
18
|
class Mpc {
|
|
19
19
|
get ready() {
|
|
20
20
|
return this._ready;
|
|
@@ -25,6 +25,47 @@ class Mpc {
|
|
|
25
25
|
constructor({ portal }) {
|
|
26
26
|
this._ready = false;
|
|
27
27
|
this.presignatureLogHandler = null;
|
|
28
|
+
this.boundFirebaseTokenBridge = (event) => {
|
|
29
|
+
var _a;
|
|
30
|
+
const { origin } = event;
|
|
31
|
+
if (origin !== this.getOrigin()) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (event.source !== ((_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { type, data } = event.data || {};
|
|
38
|
+
if (type !== 'portal:firebase:requestToken') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const requestId = data === null || data === void 0 ? void 0 : data.requestId;
|
|
42
|
+
if (!requestId) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const getter = this.firebaseGetToken;
|
|
46
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
try {
|
|
48
|
+
if (!getter) {
|
|
49
|
+
throw new Error('Firebase storage is not configured (getToken missing)');
|
|
50
|
+
}
|
|
51
|
+
const token = yield getter({
|
|
52
|
+
forceRefresh: (data === null || data === void 0 ? void 0 : data.forceRefresh) === true,
|
|
53
|
+
});
|
|
54
|
+
this.postMessage({
|
|
55
|
+
type: 'portal:firebase:requestTokenResult',
|
|
56
|
+
data: { requestId, token },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const message = (e instanceof Error ? e.message : String(e)) ||
|
|
61
|
+
'Unknown firebase token error';
|
|
62
|
+
this.postMessage({
|
|
63
|
+
type: 'portal:firebase:requestTokenError',
|
|
64
|
+
data: { requestId, message },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}))();
|
|
68
|
+
};
|
|
28
69
|
this.configureIframe = () => {
|
|
29
70
|
const config = {
|
|
30
71
|
apiKey: this.portal.apiKey,
|
|
@@ -54,6 +95,14 @@ class Mpc {
|
|
|
54
95
|
this.configureIframe = this.configureIframe.bind(this);
|
|
55
96
|
// Create the iFrame for MPC operations
|
|
56
97
|
this.appendIframe();
|
|
98
|
+
window.addEventListener('message', this.boundFirebaseTokenBridge);
|
|
99
|
+
}
|
|
100
|
+
configureFirebaseStorage(options) {
|
|
101
|
+
this.firebaseGetToken = options.getToken;
|
|
102
|
+
this.postMessage({
|
|
103
|
+
type: 'portal:firebase:configure',
|
|
104
|
+
data: { tbsHost: options.tbsHost },
|
|
105
|
+
});
|
|
57
106
|
}
|
|
58
107
|
/*******************************
|
|
59
108
|
* Wallet Methods
|
|
@@ -30,6 +30,53 @@ describe('Mpc', () => {
|
|
|
30
30
|
portal: portal_1.default,
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
|
+
describe('configureFirebaseStorage', () => {
|
|
34
|
+
it('posts configure to iframe and bridges token requests from iframe', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
|
+
const postSpy = jest
|
|
36
|
+
.spyOn(mpc.iframe.contentWindow, 'postMessage')
|
|
37
|
+
.mockImplementation(() => { });
|
|
38
|
+
mpc.configureFirebaseStorage({
|
|
39
|
+
getToken: (opts) => __awaiter(void 0, void 0, void 0, function* () { return (opts === null || opts === void 0 ? void 0 : opts.forceRefresh) ? 'firebase-jwt-refreshed' : 'firebase-jwt'; }),
|
|
40
|
+
tbsHost: 'backup.web.portalhq.io',
|
|
41
|
+
});
|
|
42
|
+
expect(postSpy).toHaveBeenNthCalledWith(1, {
|
|
43
|
+
type: 'portal:firebase:configure',
|
|
44
|
+
data: { tbsHost: 'backup.web.portalhq.io' },
|
|
45
|
+
}, mockHostOrigin);
|
|
46
|
+
const bridge = mpc
|
|
47
|
+
.boundFirebaseTokenBridge;
|
|
48
|
+
bridge(new MessageEvent('message', {
|
|
49
|
+
origin: mockHostOrigin,
|
|
50
|
+
source: mpc.iframe.contentWindow,
|
|
51
|
+
data: {
|
|
52
|
+
type: 'portal:firebase:requestToken',
|
|
53
|
+
data: { requestId: 'r1' },
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
yield new Promise((resolve) => {
|
|
57
|
+
setTimeout(resolve, 0);
|
|
58
|
+
});
|
|
59
|
+
expect(postSpy).toHaveBeenNthCalledWith(2, {
|
|
60
|
+
type: 'portal:firebase:requestTokenResult',
|
|
61
|
+
data: { requestId: 'r1', token: 'firebase-jwt' },
|
|
62
|
+
}, mockHostOrigin);
|
|
63
|
+
bridge(new MessageEvent('message', {
|
|
64
|
+
origin: mockHostOrigin,
|
|
65
|
+
source: mpc.iframe.contentWindow,
|
|
66
|
+
data: {
|
|
67
|
+
type: 'portal:firebase:requestToken',
|
|
68
|
+
data: { requestId: 'r2', forceRefresh: true },
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
yield new Promise((resolve) => {
|
|
72
|
+
setTimeout(resolve, 0);
|
|
73
|
+
});
|
|
74
|
+
expect(postSpy).toHaveBeenNthCalledWith(3, {
|
|
75
|
+
type: 'portal:firebase:requestTokenResult',
|
|
76
|
+
data: { requestId: 'r2', token: 'firebase-jwt-refreshed' },
|
|
77
|
+
}, mockHostOrigin);
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
33
80
|
describe('backup', () => {
|
|
34
81
|
const args = {
|
|
35
82
|
backupMethod: index_1.BackupMethods.password,
|
|
@@ -267,7 +314,7 @@ describe('Mpc', () => {
|
|
|
267
314
|
})
|
|
268
315
|
.catch((e) => {
|
|
269
316
|
expect(e).toBeInstanceOf(Error);
|
|
270
|
-
expect(e.message).toEqual('Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, UNKNOWN');
|
|
317
|
+
expect(e.message).toEqual('Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, FIREBASE, UNKNOWN');
|
|
271
318
|
done();
|
|
272
319
|
});
|
|
273
320
|
});
|
|
@@ -58,6 +58,7 @@ var RequestMethod;
|
|
|
58
58
|
RequestMethod["eth_sendTransaction"] = "eth_sendTransaction";
|
|
59
59
|
RequestMethod["eth_sign"] = "eth_sign";
|
|
60
60
|
RequestMethod["eth_signTransaction"] = "eth_signTransaction";
|
|
61
|
+
RequestMethod["eth_signUserOperation"] = "eth_signUserOperation";
|
|
61
62
|
RequestMethod["eth_signTypedData"] = "eth_signTypedData";
|
|
62
63
|
RequestMethod["eth_signTypedData_v3"] = "eth_signTypedData_v3";
|
|
63
64
|
RequestMethod["eth_signTypedData_v4"] = "eth_signTypedData_v4";
|
|
@@ -144,6 +145,7 @@ const signerMethods = [
|
|
|
144
145
|
RequestMethod.eth_sendTransaction,
|
|
145
146
|
RequestMethod.eth_sign,
|
|
146
147
|
RequestMethod.eth_signTransaction,
|
|
148
|
+
RequestMethod.eth_signUserOperation,
|
|
147
149
|
RequestMethod.eth_signTypedData_v3,
|
|
148
150
|
RequestMethod.eth_signTypedData_v4,
|
|
149
151
|
RequestMethod.personal_sign,
|
|
@@ -447,6 +449,7 @@ class Provider {
|
|
|
447
449
|
case RequestMethod.eth_sendTransaction:
|
|
448
450
|
case RequestMethod.eth_sign:
|
|
449
451
|
case RequestMethod.eth_signTransaction:
|
|
452
|
+
case RequestMethod.eth_signUserOperation:
|
|
450
453
|
case RequestMethod.eth_signTypedData_v3:
|
|
451
454
|
case RequestMethod.eth_signTypedData_v4:
|
|
452
455
|
case RequestMethod.personal_sign: {
|
|
@@ -206,6 +206,53 @@ describe('Provider', () => {
|
|
|
206
206
|
});
|
|
207
207
|
}));
|
|
208
208
|
});
|
|
209
|
+
describe('eth_signUserOperation', () => {
|
|
210
|
+
const mockUserOperation = {
|
|
211
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
212
|
+
callData: '0x',
|
|
213
|
+
nonce: '0x0',
|
|
214
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
215
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
216
|
+
signature: '0x',
|
|
217
|
+
};
|
|
218
|
+
it('should throw an error if no chainId is provided alongside eth_signUserOperation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
expect(provider.request({
|
|
220
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
221
|
+
params: [mockUserOperation],
|
|
222
|
+
})).rejects.toThrow(new Error(`[PortalProvider] Chain ID is required for the operation`));
|
|
223
|
+
}));
|
|
224
|
+
it('should throw an error if malformed chainId is provided alongside eth_signUserOperation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
225
|
+
const chainId = 'unsupported:chain';
|
|
226
|
+
expect(provider.request({
|
|
227
|
+
chainId,
|
|
228
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
229
|
+
params: [mockUserOperation],
|
|
230
|
+
})).rejects.toThrow(new Error(`[PortalProvider] Chain ID must be prefixed with "eip155:" for the operation, got ${chainId}`));
|
|
231
|
+
}));
|
|
232
|
+
it('should successfully handle an eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
233
|
+
const result = yield provider.request({
|
|
234
|
+
chainId: 'eip155:1',
|
|
235
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
236
|
+
params: [mockUserOperation],
|
|
237
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
238
|
+
});
|
|
239
|
+
expect(result).toEqual(constants_1.mockSignedHash);
|
|
240
|
+
expect(portal_1.default.mpc.sign).toHaveBeenCalledWith({
|
|
241
|
+
chainId: 'eip155:1',
|
|
242
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
243
|
+
params: mockUserOperation,
|
|
244
|
+
rpcUrl: constants_1.mockRpcUrl,
|
|
245
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
246
|
+
traceId: 'mock-trace-id-12345',
|
|
247
|
+
});
|
|
248
|
+
expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
|
|
249
|
+
chainId: 'eip155:1',
|
|
250
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
251
|
+
params: [mockUserOperation],
|
|
252
|
+
signature: result,
|
|
253
|
+
});
|
|
254
|
+
}));
|
|
255
|
+
});
|
|
209
256
|
describe('eth_sign', () => {
|
|
210
257
|
it('should throw an error if no chainId is provided alongside eth_sign', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
258
|
expect(provider.request({
|
|
@@ -674,6 +721,69 @@ describe('Provider', () => {
|
|
|
674
721
|
expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'eth_signTransaction' could not be completed because it was not approved by the user.");
|
|
675
722
|
}));
|
|
676
723
|
});
|
|
724
|
+
describe('eth_signUserOperation', () => {
|
|
725
|
+
const mockUserOperation = {
|
|
726
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
727
|
+
callData: '0x',
|
|
728
|
+
nonce: '0x0',
|
|
729
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
730
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
731
|
+
signature: '0x',
|
|
732
|
+
};
|
|
733
|
+
it('should successfully handle an approved eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
734
|
+
provider.on('portal_signingRequested', () => {
|
|
735
|
+
provider.emit('portal_signingApproved', {
|
|
736
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
737
|
+
params: [mockUserOperation],
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
const result = yield provider.request({
|
|
741
|
+
chainId: 'eip155:1',
|
|
742
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
743
|
+
params: [mockUserOperation],
|
|
744
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
745
|
+
});
|
|
746
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
747
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
748
|
+
params: [mockUserOperation],
|
|
749
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
750
|
+
});
|
|
751
|
+
expect(result).toEqual(constants_1.mockSignedHash);
|
|
752
|
+
expect(portal_1.default.mpc.sign).toHaveBeenCalledWith({
|
|
753
|
+
chainId: 'eip155:1',
|
|
754
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
755
|
+
params: mockUserOperation,
|
|
756
|
+
rpcUrl: constants_1.mockRpcUrl,
|
|
757
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
758
|
+
traceId: 'mock-trace-id-12345',
|
|
759
|
+
});
|
|
760
|
+
expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
|
|
761
|
+
chainId: 'eip155:1',
|
|
762
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
763
|
+
params: [mockUserOperation],
|
|
764
|
+
signature: result,
|
|
765
|
+
});
|
|
766
|
+
}));
|
|
767
|
+
it('should successfully handle a rejected eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
768
|
+
provider.on('portal_signingRequested', () => {
|
|
769
|
+
provider.emit('portal_signingRejected', {
|
|
770
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
771
|
+
params: [mockUserOperation],
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
const result = yield provider.request({
|
|
775
|
+
chainId: 'eip155:1',
|
|
776
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
777
|
+
params: [mockUserOperation],
|
|
778
|
+
});
|
|
779
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
780
|
+
method: __1.RequestMethod.eth_signUserOperation,
|
|
781
|
+
params: [mockUserOperation],
|
|
782
|
+
});
|
|
783
|
+
expect(result).toEqual(undefined);
|
|
784
|
+
expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'eth_signUserOperation' could not be completed because it was not approved by the user.");
|
|
785
|
+
}));
|
|
786
|
+
});
|
|
677
787
|
describe('eth_sign', () => {
|
|
678
788
|
it('should successfully handle an approved eth_sign request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
679
789
|
provider.on('portal_signingRequested', () => {
|
package/lib/esm/index.js
CHANGED
|
@@ -287,6 +287,9 @@ class Portal {
|
|
|
287
287
|
yield this.storedClientBackupShare(true, BackupMethods.passkey);
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
|
+
configureFirebaseStorage(options) {
|
|
291
|
+
this.mpc.configureFirebaseStorage(options);
|
|
292
|
+
}
|
|
290
293
|
backupWallet(backupMethod, progress = () => {
|
|
291
294
|
// Noop
|
|
292
295
|
}, backupConfigs = {}) {
|
|
@@ -1111,6 +1114,7 @@ export var BackupMethods;
|
|
|
1111
1114
|
BackupMethods["password"] = "PASSWORD";
|
|
1112
1115
|
BackupMethods["passkey"] = "PASSKEY";
|
|
1113
1116
|
BackupMethods["custom"] = "CUSTOM";
|
|
1117
|
+
BackupMethods["firebase"] = "FIREBASE";
|
|
1114
1118
|
BackupMethods["unknown"] = "UNKNOWN";
|
|
1115
1119
|
})(BackupMethods || (BackupMethods = {}));
|
|
1116
1120
|
export var GetTransactionsOrder;
|
package/lib/esm/mpc/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { PortalMpcError } from './errors';
|
|
|
11
11
|
import { sdkLogger } from '../logger';
|
|
12
12
|
import { BackupMethods, } from '../index';
|
|
13
13
|
import { generateTraceId } from '../shared/trace';
|
|
14
|
-
const WEB_SDK_VERSION = '3.
|
|
14
|
+
const WEB_SDK_VERSION = '3.15.0-alpha.2';
|
|
15
15
|
class Mpc {
|
|
16
16
|
get ready() {
|
|
17
17
|
return this._ready;
|
|
@@ -22,6 +22,47 @@ class Mpc {
|
|
|
22
22
|
constructor({ portal }) {
|
|
23
23
|
this._ready = false;
|
|
24
24
|
this.presignatureLogHandler = null;
|
|
25
|
+
this.boundFirebaseTokenBridge = (event) => {
|
|
26
|
+
var _a;
|
|
27
|
+
const { origin } = event;
|
|
28
|
+
if (origin !== this.getOrigin()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (event.source !== ((_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { type, data } = event.data || {};
|
|
35
|
+
if (type !== 'portal:firebase:requestToken') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const requestId = data === null || data === void 0 ? void 0 : data.requestId;
|
|
39
|
+
if (!requestId) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const getter = this.firebaseGetToken;
|
|
43
|
+
void (() => __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
try {
|
|
45
|
+
if (!getter) {
|
|
46
|
+
throw new Error('Firebase storage is not configured (getToken missing)');
|
|
47
|
+
}
|
|
48
|
+
const token = yield getter({
|
|
49
|
+
forceRefresh: (data === null || data === void 0 ? void 0 : data.forceRefresh) === true,
|
|
50
|
+
});
|
|
51
|
+
this.postMessage({
|
|
52
|
+
type: 'portal:firebase:requestTokenResult',
|
|
53
|
+
data: { requestId, token },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const message = (e instanceof Error ? e.message : String(e)) ||
|
|
58
|
+
'Unknown firebase token error';
|
|
59
|
+
this.postMessage({
|
|
60
|
+
type: 'portal:firebase:requestTokenError',
|
|
61
|
+
data: { requestId, message },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}))();
|
|
65
|
+
};
|
|
25
66
|
this.configureIframe = () => {
|
|
26
67
|
const config = {
|
|
27
68
|
apiKey: this.portal.apiKey,
|
|
@@ -51,6 +92,14 @@ class Mpc {
|
|
|
51
92
|
this.configureIframe = this.configureIframe.bind(this);
|
|
52
93
|
// Create the iFrame for MPC operations
|
|
53
94
|
this.appendIframe();
|
|
95
|
+
window.addEventListener('message', this.boundFirebaseTokenBridge);
|
|
96
|
+
}
|
|
97
|
+
configureFirebaseStorage(options) {
|
|
98
|
+
this.firebaseGetToken = options.getToken;
|
|
99
|
+
this.postMessage({
|
|
100
|
+
type: 'portal:firebase:configure',
|
|
101
|
+
data: { tbsHost: options.tbsHost },
|
|
102
|
+
});
|
|
54
103
|
}
|
|
55
104
|
/*******************************
|
|
56
105
|
* Wallet Methods
|
|
@@ -25,6 +25,53 @@ describe('Mpc', () => {
|
|
|
25
25
|
portal: portalMock,
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
+
describe('configureFirebaseStorage', () => {
|
|
29
|
+
it('posts configure to iframe and bridges token requests from iframe', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
const postSpy = jest
|
|
31
|
+
.spyOn(mpc.iframe.contentWindow, 'postMessage')
|
|
32
|
+
.mockImplementation(() => { });
|
|
33
|
+
mpc.configureFirebaseStorage({
|
|
34
|
+
getToken: (opts) => __awaiter(void 0, void 0, void 0, function* () { return (opts === null || opts === void 0 ? void 0 : opts.forceRefresh) ? 'firebase-jwt-refreshed' : 'firebase-jwt'; }),
|
|
35
|
+
tbsHost: 'backup.web.portalhq.io',
|
|
36
|
+
});
|
|
37
|
+
expect(postSpy).toHaveBeenNthCalledWith(1, {
|
|
38
|
+
type: 'portal:firebase:configure',
|
|
39
|
+
data: { tbsHost: 'backup.web.portalhq.io' },
|
|
40
|
+
}, mockHostOrigin);
|
|
41
|
+
const bridge = mpc
|
|
42
|
+
.boundFirebaseTokenBridge;
|
|
43
|
+
bridge(new MessageEvent('message', {
|
|
44
|
+
origin: mockHostOrigin,
|
|
45
|
+
source: mpc.iframe.contentWindow,
|
|
46
|
+
data: {
|
|
47
|
+
type: 'portal:firebase:requestToken',
|
|
48
|
+
data: { requestId: 'r1' },
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
yield new Promise((resolve) => {
|
|
52
|
+
setTimeout(resolve, 0);
|
|
53
|
+
});
|
|
54
|
+
expect(postSpy).toHaveBeenNthCalledWith(2, {
|
|
55
|
+
type: 'portal:firebase:requestTokenResult',
|
|
56
|
+
data: { requestId: 'r1', token: 'firebase-jwt' },
|
|
57
|
+
}, mockHostOrigin);
|
|
58
|
+
bridge(new MessageEvent('message', {
|
|
59
|
+
origin: mockHostOrigin,
|
|
60
|
+
source: mpc.iframe.contentWindow,
|
|
61
|
+
data: {
|
|
62
|
+
type: 'portal:firebase:requestToken',
|
|
63
|
+
data: { requestId: 'r2', forceRefresh: true },
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
yield new Promise((resolve) => {
|
|
67
|
+
setTimeout(resolve, 0);
|
|
68
|
+
});
|
|
69
|
+
expect(postSpy).toHaveBeenNthCalledWith(3, {
|
|
70
|
+
type: 'portal:firebase:requestTokenResult',
|
|
71
|
+
data: { requestId: 'r2', token: 'firebase-jwt-refreshed' },
|
|
72
|
+
}, mockHostOrigin);
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
28
75
|
describe('backup', () => {
|
|
29
76
|
const args = {
|
|
30
77
|
backupMethod: BackupMethods.password,
|
|
@@ -262,7 +309,7 @@ describe('Mpc', () => {
|
|
|
262
309
|
})
|
|
263
310
|
.catch((e) => {
|
|
264
311
|
expect(e).toBeInstanceOf(Error);
|
|
265
|
-
expect(e.message).toEqual('Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, UNKNOWN');
|
|
312
|
+
expect(e.message).toEqual('Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, FIREBASE, UNKNOWN');
|
|
266
313
|
done();
|
|
267
314
|
});
|
|
268
315
|
});
|
|
@@ -55,6 +55,7 @@ export var RequestMethod;
|
|
|
55
55
|
RequestMethod["eth_sendTransaction"] = "eth_sendTransaction";
|
|
56
56
|
RequestMethod["eth_sign"] = "eth_sign";
|
|
57
57
|
RequestMethod["eth_signTransaction"] = "eth_signTransaction";
|
|
58
|
+
RequestMethod["eth_signUserOperation"] = "eth_signUserOperation";
|
|
58
59
|
RequestMethod["eth_signTypedData"] = "eth_signTypedData";
|
|
59
60
|
RequestMethod["eth_signTypedData_v3"] = "eth_signTypedData_v3";
|
|
60
61
|
RequestMethod["eth_signTypedData_v4"] = "eth_signTypedData_v4";
|
|
@@ -141,6 +142,7 @@ const signerMethods = [
|
|
|
141
142
|
RequestMethod.eth_sendTransaction,
|
|
142
143
|
RequestMethod.eth_sign,
|
|
143
144
|
RequestMethod.eth_signTransaction,
|
|
145
|
+
RequestMethod.eth_signUserOperation,
|
|
144
146
|
RequestMethod.eth_signTypedData_v3,
|
|
145
147
|
RequestMethod.eth_signTypedData_v4,
|
|
146
148
|
RequestMethod.personal_sign,
|
|
@@ -444,6 +446,7 @@ class Provider {
|
|
|
444
446
|
case RequestMethod.eth_sendTransaction:
|
|
445
447
|
case RequestMethod.eth_sign:
|
|
446
448
|
case RequestMethod.eth_signTransaction:
|
|
449
|
+
case RequestMethod.eth_signUserOperation:
|
|
447
450
|
case RequestMethod.eth_signTypedData_v3:
|
|
448
451
|
case RequestMethod.eth_signTypedData_v4:
|
|
449
452
|
case RequestMethod.personal_sign: {
|
|
@@ -201,6 +201,53 @@ describe('Provider', () => {
|
|
|
201
201
|
});
|
|
202
202
|
}));
|
|
203
203
|
});
|
|
204
|
+
describe('eth_signUserOperation', () => {
|
|
205
|
+
const mockUserOperation = {
|
|
206
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
207
|
+
callData: '0x',
|
|
208
|
+
nonce: '0x0',
|
|
209
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
210
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
211
|
+
signature: '0x',
|
|
212
|
+
};
|
|
213
|
+
it('should throw an error if no chainId is provided alongside eth_signUserOperation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
expect(provider.request({
|
|
215
|
+
method: RequestMethod.eth_signUserOperation,
|
|
216
|
+
params: [mockUserOperation],
|
|
217
|
+
})).rejects.toThrow(new Error(`[PortalProvider] Chain ID is required for the operation`));
|
|
218
|
+
}));
|
|
219
|
+
it('should throw an error if malformed chainId is provided alongside eth_signUserOperation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
const chainId = 'unsupported:chain';
|
|
221
|
+
expect(provider.request({
|
|
222
|
+
chainId,
|
|
223
|
+
method: RequestMethod.eth_signUserOperation,
|
|
224
|
+
params: [mockUserOperation],
|
|
225
|
+
})).rejects.toThrow(new Error(`[PortalProvider] Chain ID must be prefixed with "eip155:" for the operation, got ${chainId}`));
|
|
226
|
+
}));
|
|
227
|
+
it('should successfully handle an eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
228
|
+
const result = yield provider.request({
|
|
229
|
+
chainId: 'eip155:1',
|
|
230
|
+
method: RequestMethod.eth_signUserOperation,
|
|
231
|
+
params: [mockUserOperation],
|
|
232
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
233
|
+
});
|
|
234
|
+
expect(result).toEqual(mockSignedHash);
|
|
235
|
+
expect(portal.mpc.sign).toHaveBeenCalledWith({
|
|
236
|
+
chainId: 'eip155:1',
|
|
237
|
+
method: RequestMethod.eth_signUserOperation,
|
|
238
|
+
params: mockUserOperation,
|
|
239
|
+
rpcUrl: mockRpcUrl,
|
|
240
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
241
|
+
traceId: 'mock-trace-id-12345',
|
|
242
|
+
});
|
|
243
|
+
expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
|
|
244
|
+
chainId: 'eip155:1',
|
|
245
|
+
method: RequestMethod.eth_signUserOperation,
|
|
246
|
+
params: [mockUserOperation],
|
|
247
|
+
signature: result,
|
|
248
|
+
});
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
204
251
|
describe('eth_sign', () => {
|
|
205
252
|
it('should throw an error if no chainId is provided alongside eth_sign', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
253
|
expect(provider.request({
|
|
@@ -669,6 +716,69 @@ describe('Provider', () => {
|
|
|
669
716
|
expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'eth_signTransaction' could not be completed because it was not approved by the user.");
|
|
670
717
|
}));
|
|
671
718
|
});
|
|
719
|
+
describe('eth_signUserOperation', () => {
|
|
720
|
+
const mockUserOperation = {
|
|
721
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
722
|
+
callData: '0x',
|
|
723
|
+
nonce: '0x0',
|
|
724
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
725
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
726
|
+
signature: '0x',
|
|
727
|
+
};
|
|
728
|
+
it('should successfully handle an approved eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
729
|
+
provider.on('portal_signingRequested', () => {
|
|
730
|
+
provider.emit('portal_signingApproved', {
|
|
731
|
+
method: RequestMethod.eth_signUserOperation,
|
|
732
|
+
params: [mockUserOperation],
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
const result = yield provider.request({
|
|
736
|
+
chainId: 'eip155:1',
|
|
737
|
+
method: RequestMethod.eth_signUserOperation,
|
|
738
|
+
params: [mockUserOperation],
|
|
739
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
740
|
+
});
|
|
741
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
742
|
+
method: RequestMethod.eth_signUserOperation,
|
|
743
|
+
params: [mockUserOperation],
|
|
744
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
745
|
+
});
|
|
746
|
+
expect(result).toEqual(mockSignedHash);
|
|
747
|
+
expect(portal.mpc.sign).toHaveBeenCalledWith({
|
|
748
|
+
chainId: 'eip155:1',
|
|
749
|
+
method: RequestMethod.eth_signUserOperation,
|
|
750
|
+
params: mockUserOperation,
|
|
751
|
+
rpcUrl: mockRpcUrl,
|
|
752
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
753
|
+
traceId: 'mock-trace-id-12345',
|
|
754
|
+
});
|
|
755
|
+
expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
|
|
756
|
+
chainId: 'eip155:1',
|
|
757
|
+
method: RequestMethod.eth_signUserOperation,
|
|
758
|
+
params: [mockUserOperation],
|
|
759
|
+
signature: result,
|
|
760
|
+
});
|
|
761
|
+
}));
|
|
762
|
+
it('should successfully handle a rejected eth_signUserOperation request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
763
|
+
provider.on('portal_signingRequested', () => {
|
|
764
|
+
provider.emit('portal_signingRejected', {
|
|
765
|
+
method: RequestMethod.eth_signUserOperation,
|
|
766
|
+
params: [mockUserOperation],
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
const result = yield provider.request({
|
|
770
|
+
chainId: 'eip155:1',
|
|
771
|
+
method: RequestMethod.eth_signUserOperation,
|
|
772
|
+
params: [mockUserOperation],
|
|
773
|
+
});
|
|
774
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
775
|
+
method: RequestMethod.eth_signUserOperation,
|
|
776
|
+
params: [mockUserOperation],
|
|
777
|
+
});
|
|
778
|
+
expect(result).toEqual(undefined);
|
|
779
|
+
expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'eth_signUserOperation' could not be completed because it was not approved by the user.");
|
|
780
|
+
}));
|
|
781
|
+
});
|
|
672
782
|
describe('eth_sign', () => {
|
|
673
783
|
it('should successfully handle an approved eth_sign request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
674
784
|
provider.on('portal_signingRequested', () => {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Portal MPC Support for Web",
|
|
4
4
|
"author": "Portal Labs, Inc.",
|
|
5
5
|
"homepage": "https://portalhq.io/",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.15.0-alpha.2",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "lib/commonjs/index",
|
|
9
9
|
"module": "lib/esm/index",
|
|
@@ -58,5 +58,6 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@solana/web3.js": "^1.91.8"
|
|
61
|
-
}
|
|
61
|
+
},
|
|
62
|
+
"stableVersion": "3.15.0-alpha.1"
|
|
62
63
|
}
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type BackupShareResult,
|
|
49
49
|
type PasskeyOptions,
|
|
50
50
|
type PortalOptions,
|
|
51
|
+
type FirebaseStorageConfigOptions,
|
|
51
52
|
} from '../types'
|
|
52
53
|
import Mpc from './mpc'
|
|
53
54
|
import Provider, { RequestMethod } from './provider'
|
|
@@ -434,6 +435,12 @@ class Portal {
|
|
|
434
435
|
await this.storedClientBackupShare(true, BackupMethods.passkey)
|
|
435
436
|
}
|
|
436
437
|
|
|
438
|
+
public configureFirebaseStorage(
|
|
439
|
+
options: FirebaseStorageConfigOptions,
|
|
440
|
+
): void {
|
|
441
|
+
this.mpc.configureFirebaseStorage(options)
|
|
442
|
+
}
|
|
443
|
+
|
|
437
444
|
public async backupWallet(
|
|
438
445
|
backupMethod: BackupMethods,
|
|
439
446
|
progress: ProgressCallback = () => {
|
|
@@ -1568,6 +1575,7 @@ export { PortalMpcError } from './mpc/errors'
|
|
|
1568
1575
|
export {
|
|
1569
1576
|
type Address,
|
|
1570
1577
|
type Dapp,
|
|
1578
|
+
type FirebaseStorageConfigOptions,
|
|
1571
1579
|
type ILogger,
|
|
1572
1580
|
type LogLevel,
|
|
1573
1581
|
type MpcStatus,
|
|
@@ -1592,6 +1600,7 @@ export enum BackupMethods {
|
|
|
1592
1600
|
password = 'PASSWORD',
|
|
1593
1601
|
passkey = 'PASSKEY',
|
|
1594
1602
|
custom = 'CUSTOM',
|
|
1603
|
+
firebase = 'FIREBASE',
|
|
1595
1604
|
unknown = 'UNKNOWN',
|
|
1596
1605
|
}
|
|
1597
1606
|
|
package/src/mpc/index.test.ts
CHANGED
|
@@ -99,6 +99,75 @@ describe('Mpc', () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
describe('configureFirebaseStorage', () => {
|
|
103
|
+
it('posts configure to iframe and bridges token requests from iframe', async () => {
|
|
104
|
+
const postSpy = jest
|
|
105
|
+
.spyOn(mpc.iframe!.contentWindow!, 'postMessage')
|
|
106
|
+
.mockImplementation(() => {})
|
|
107
|
+
|
|
108
|
+
mpc.configureFirebaseStorage({
|
|
109
|
+
getToken: async (opts) =>
|
|
110
|
+
opts?.forceRefresh ? 'firebase-jwt-refreshed' : 'firebase-jwt',
|
|
111
|
+
tbsHost: 'backup.web.portalhq.io',
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(postSpy).toHaveBeenNthCalledWith(
|
|
115
|
+
1,
|
|
116
|
+
{
|
|
117
|
+
type: 'portal:firebase:configure',
|
|
118
|
+
data: { tbsHost: 'backup.web.portalhq.io' },
|
|
119
|
+
},
|
|
120
|
+
mockHostOrigin,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const bridge = (mpc as unknown as { boundFirebaseTokenBridge: (e: MessageEvent) => void })
|
|
124
|
+
.boundFirebaseTokenBridge
|
|
125
|
+
bridge(
|
|
126
|
+
new MessageEvent('message', {
|
|
127
|
+
origin: mockHostOrigin,
|
|
128
|
+
source: mpc.iframe!.contentWindow!,
|
|
129
|
+
data: {
|
|
130
|
+
type: 'portal:firebase:requestToken',
|
|
131
|
+
data: { requestId: 'r1' },
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
await new Promise<void>((resolve) => {
|
|
136
|
+
setTimeout(resolve, 0)
|
|
137
|
+
})
|
|
138
|
+
expect(postSpy).toHaveBeenNthCalledWith(
|
|
139
|
+
2,
|
|
140
|
+
{
|
|
141
|
+
type: 'portal:firebase:requestTokenResult',
|
|
142
|
+
data: { requestId: 'r1', token: 'firebase-jwt' },
|
|
143
|
+
},
|
|
144
|
+
mockHostOrigin,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
bridge(
|
|
148
|
+
new MessageEvent('message', {
|
|
149
|
+
origin: mockHostOrigin,
|
|
150
|
+
source: mpc.iframe!.contentWindow!,
|
|
151
|
+
data: {
|
|
152
|
+
type: 'portal:firebase:requestToken',
|
|
153
|
+
data: { requestId: 'r2', forceRefresh: true },
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
await new Promise<void>((resolve) => {
|
|
158
|
+
setTimeout(resolve, 0)
|
|
159
|
+
})
|
|
160
|
+
expect(postSpy).toHaveBeenNthCalledWith(
|
|
161
|
+
3,
|
|
162
|
+
{
|
|
163
|
+
type: 'portal:firebase:requestTokenResult',
|
|
164
|
+
data: { requestId: 'r2', token: 'firebase-jwt-refreshed' },
|
|
165
|
+
},
|
|
166
|
+
mockHostOrigin,
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
102
171
|
describe('backup', () => {
|
|
103
172
|
const args = {
|
|
104
173
|
backupMethod: BackupMethods.password,
|
|
@@ -371,7 +440,7 @@ describe('Mpc', () => {
|
|
|
371
440
|
.catch((e) => {
|
|
372
441
|
expect(e).toBeInstanceOf(Error)
|
|
373
442
|
expect(e.message).toEqual(
|
|
374
|
-
'Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, UNKNOWN',
|
|
443
|
+
'Invalid backup method: INVALID_METHOD. Valid methods are: GDRIVE, PASSWORD, PASSKEY, CUSTOM, FIREBASE, UNKNOWN',
|
|
375
444
|
)
|
|
376
445
|
done()
|
|
377
446
|
})
|
package/src/mpc/index.ts
CHANGED
|
@@ -134,7 +134,7 @@ import {
|
|
|
134
134
|
} from '../../hypernative'
|
|
135
135
|
import { generateTraceId } from '../shared/trace'
|
|
136
136
|
|
|
137
|
-
const WEB_SDK_VERSION = '3.
|
|
137
|
+
const WEB_SDK_VERSION = '3.15.0-alpha.2'
|
|
138
138
|
|
|
139
139
|
class Mpc {
|
|
140
140
|
public iframe?: HTMLIFrameElement
|
|
@@ -142,6 +142,52 @@ class Mpc {
|
|
|
142
142
|
private portal: Portal
|
|
143
143
|
private _ready = false
|
|
144
144
|
private presignatureLogHandler: ((event: MessageEvent) => void) | null = null
|
|
145
|
+
private firebaseGetToken?: (
|
|
146
|
+
options?: { forceRefresh?: boolean },
|
|
147
|
+
) => Promise<string | null>
|
|
148
|
+
|
|
149
|
+
private boundFirebaseTokenBridge = (event: MessageEvent) => {
|
|
150
|
+
const { origin } = event
|
|
151
|
+
if (origin !== this.getOrigin()) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (event.source !== this.iframe?.contentWindow) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const { type, data } = event.data || {}
|
|
158
|
+
if (type !== 'portal:firebase:requestToken') {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const requestId = data?.requestId as string | undefined
|
|
162
|
+
if (!requestId) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
const getter = this.firebaseGetToken
|
|
166
|
+
void (async () => {
|
|
167
|
+
try {
|
|
168
|
+
if (!getter) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'Firebase storage is not configured (getToken missing)',
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
const token = await getter({
|
|
174
|
+
forceRefresh: data?.forceRefresh === true,
|
|
175
|
+
})
|
|
176
|
+
this.postMessage({
|
|
177
|
+
type: 'portal:firebase:requestTokenResult',
|
|
178
|
+
data: { requestId, token },
|
|
179
|
+
})
|
|
180
|
+
} catch (e) {
|
|
181
|
+
const message =
|
|
182
|
+
(e instanceof Error ? e.message : String(e)) ||
|
|
183
|
+
'Unknown firebase token error'
|
|
184
|
+
this.postMessage({
|
|
185
|
+
type: 'portal:firebase:requestTokenError',
|
|
186
|
+
data: { requestId, message },
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
})()
|
|
190
|
+
}
|
|
145
191
|
|
|
146
192
|
public get ready() {
|
|
147
193
|
return this._ready
|
|
@@ -159,6 +205,18 @@ class Mpc {
|
|
|
159
205
|
|
|
160
206
|
// Create the iFrame for MPC operations
|
|
161
207
|
this.appendIframe()
|
|
208
|
+
window.addEventListener('message', this.boundFirebaseTokenBridge)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public configureFirebaseStorage(options: {
|
|
212
|
+
getToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>
|
|
213
|
+
tbsHost?: string
|
|
214
|
+
}): void {
|
|
215
|
+
this.firebaseGetToken = options.getToken
|
|
216
|
+
this.postMessage({
|
|
217
|
+
type: 'portal:firebase:configure',
|
|
218
|
+
data: { tbsHost: options.tbsHost },
|
|
219
|
+
})
|
|
162
220
|
}
|
|
163
221
|
|
|
164
222
|
/*******************************
|
|
@@ -298,6 +298,73 @@ describe('Provider', () => {
|
|
|
298
298
|
})
|
|
299
299
|
})
|
|
300
300
|
|
|
301
|
+
describe('eth_signUserOperation', () => {
|
|
302
|
+
const mockUserOperation = {
|
|
303
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
304
|
+
callData: '0x',
|
|
305
|
+
nonce: '0x0',
|
|
306
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
307
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
308
|
+
signature: '0x',
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it('should throw an error if no chainId is provided alongside eth_signUserOperation', async () => {
|
|
312
|
+
expect(
|
|
313
|
+
provider.request({
|
|
314
|
+
method: RequestMethod.eth_signUserOperation,
|
|
315
|
+
params: [mockUserOperation],
|
|
316
|
+
}),
|
|
317
|
+
).rejects.toThrow(
|
|
318
|
+
new Error(
|
|
319
|
+
`[PortalProvider] Chain ID is required for the operation`,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should throw an error if malformed chainId is provided alongside eth_signUserOperation', async () => {
|
|
325
|
+
const chainId = 'unsupported:chain'
|
|
326
|
+
expect(
|
|
327
|
+
provider.request({
|
|
328
|
+
chainId,
|
|
329
|
+
method: RequestMethod.eth_signUserOperation,
|
|
330
|
+
params: [mockUserOperation],
|
|
331
|
+
}),
|
|
332
|
+
).rejects.toThrow(
|
|
333
|
+
new Error(
|
|
334
|
+
`[PortalProvider] Chain ID must be prefixed with "eip155:" for the operation, got ${chainId}`,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should successfully handle an eth_signUserOperation request', async () => {
|
|
340
|
+
const result = await provider.request({
|
|
341
|
+
chainId: 'eip155:1',
|
|
342
|
+
method: RequestMethod.eth_signUserOperation,
|
|
343
|
+
params: [mockUserOperation],
|
|
344
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
expect(result).toEqual(mockSignedHash)
|
|
348
|
+
expect(portal.mpc.sign).toHaveBeenCalledWith({
|
|
349
|
+
chainId: 'eip155:1',
|
|
350
|
+
method: RequestMethod.eth_signUserOperation,
|
|
351
|
+
params: mockUserOperation,
|
|
352
|
+
rpcUrl: mockRpcUrl,
|
|
353
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
354
|
+
traceId: 'mock-trace-id-12345',
|
|
355
|
+
})
|
|
356
|
+
expect(provider.emit).toHaveBeenCalledWith(
|
|
357
|
+
'portal_signatureReceived',
|
|
358
|
+
{
|
|
359
|
+
chainId: 'eip155:1',
|
|
360
|
+
method: RequestMethod.eth_signUserOperation,
|
|
361
|
+
params: [mockUserOperation],
|
|
362
|
+
signature: result,
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
301
368
|
describe('eth_sign', () => {
|
|
302
369
|
it('should throw an error if no chainId is provided alongside eth_sign', async () => {
|
|
303
370
|
expect(
|
|
@@ -939,6 +1006,76 @@ describe('Provider', () => {
|
|
|
939
1006
|
})
|
|
940
1007
|
})
|
|
941
1008
|
|
|
1009
|
+
describe('eth_signUserOperation', () => {
|
|
1010
|
+
const mockUserOperation = {
|
|
1011
|
+
sender: '0x1234567890123456789012345678901234567890',
|
|
1012
|
+
callData: '0x',
|
|
1013
|
+
nonce: '0x0',
|
|
1014
|
+
maxFeePerGas: '0x3B9ACA00',
|
|
1015
|
+
maxPriorityFeePerGas: '0x3B9ACA00',
|
|
1016
|
+
signature: '0x',
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
it('should successfully handle an approved eth_signUserOperation request', async () => {
|
|
1020
|
+
provider.on('portal_signingRequested', () => {
|
|
1021
|
+
provider.emit('portal_signingApproved', {
|
|
1022
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1023
|
+
params: [mockUserOperation],
|
|
1024
|
+
})
|
|
1025
|
+
})
|
|
1026
|
+
const result = await provider.request({
|
|
1027
|
+
chainId: 'eip155:1',
|
|
1028
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1029
|
+
params: [mockUserOperation],
|
|
1030
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
1034
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1035
|
+
params: [mockUserOperation],
|
|
1036
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
1037
|
+
})
|
|
1038
|
+
expect(result).toEqual(mockSignedHash)
|
|
1039
|
+
expect(portal.mpc.sign).toHaveBeenCalledWith({
|
|
1040
|
+
chainId: 'eip155:1',
|
|
1041
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1042
|
+
params: mockUserOperation,
|
|
1043
|
+
rpcUrl: mockRpcUrl,
|
|
1044
|
+
signatureApprovalMemo: 'Test eth_signUserOperation',
|
|
1045
|
+
traceId: 'mock-trace-id-12345',
|
|
1046
|
+
})
|
|
1047
|
+
expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
|
|
1048
|
+
chainId: 'eip155:1',
|
|
1049
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1050
|
+
params: [mockUserOperation],
|
|
1051
|
+
signature: result,
|
|
1052
|
+
})
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
it('should successfully handle a rejected eth_signUserOperation request', async () => {
|
|
1056
|
+
provider.on('portal_signingRequested', () => {
|
|
1057
|
+
provider.emit('portal_signingRejected', {
|
|
1058
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1059
|
+
params: [mockUserOperation],
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
|
|
1063
|
+
const result = await provider.request({
|
|
1064
|
+
chainId: 'eip155:1',
|
|
1065
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1066
|
+
params: [mockUserOperation],
|
|
1067
|
+
})
|
|
1068
|
+
expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
|
|
1069
|
+
method: RequestMethod.eth_signUserOperation,
|
|
1070
|
+
params: [mockUserOperation],
|
|
1071
|
+
})
|
|
1072
|
+
expect(result).toEqual(undefined)
|
|
1073
|
+
expect(mockConsoleWarn).toHaveBeenCalledWith(
|
|
1074
|
+
"[PortalProvider] Request for signing method 'eth_signUserOperation' could not be completed because it was not approved by the user.",
|
|
1075
|
+
)
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
|
|
942
1079
|
describe('eth_sign', () => {
|
|
943
1080
|
it('should successfully handle an approved eth_sign request', async () => {
|
|
944
1081
|
provider.on('portal_signingRequested', () => {
|
package/src/provider/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export enum RequestMethod {
|
|
|
56
56
|
eth_sendTransaction = 'eth_sendTransaction',
|
|
57
57
|
eth_sign = 'eth_sign',
|
|
58
58
|
eth_signTransaction = 'eth_signTransaction',
|
|
59
|
+
eth_signUserOperation = 'eth_signUserOperation',
|
|
59
60
|
eth_signTypedData = 'eth_signTypedData',
|
|
60
61
|
eth_signTypedData_v3 = 'eth_signTypedData_v3',
|
|
61
62
|
eth_signTypedData_v4 = 'eth_signTypedData_v4',
|
|
@@ -148,6 +149,7 @@ const signerMethods = [
|
|
|
148
149
|
RequestMethod.eth_sendTransaction,
|
|
149
150
|
RequestMethod.eth_sign,
|
|
150
151
|
RequestMethod.eth_signTransaction,
|
|
152
|
+
RequestMethod.eth_signUserOperation,
|
|
151
153
|
RequestMethod.eth_signTypedData_v3,
|
|
152
154
|
RequestMethod.eth_signTypedData_v4,
|
|
153
155
|
RequestMethod.personal_sign,
|
|
@@ -502,6 +504,7 @@ class Provider {
|
|
|
502
504
|
case RequestMethod.eth_sendTransaction:
|
|
503
505
|
case RequestMethod.eth_sign:
|
|
504
506
|
case RequestMethod.eth_signTransaction:
|
|
507
|
+
case RequestMethod.eth_signUserOperation:
|
|
505
508
|
case RequestMethod.eth_signTypedData_v3:
|
|
506
509
|
case RequestMethod.eth_signTypedData_v4:
|
|
507
510
|
case RequestMethod.personal_sign: {
|
|
@@ -701,7 +701,13 @@ export interface CustomBackupConfig {
|
|
|
701
701
|
}
|
|
702
702
|
|
|
703
703
|
// Backup method types - matches enum values from both browser and iframe packages
|
|
704
|
-
export type BackupMethod =
|
|
704
|
+
export type BackupMethod =
|
|
705
|
+
| 'GDRIVE'
|
|
706
|
+
| 'PASSWORD'
|
|
707
|
+
| 'PASSKEY'
|
|
708
|
+
| 'CUSTOM'
|
|
709
|
+
| 'FIREBASE'
|
|
710
|
+
| 'UNKNOWN'
|
|
705
711
|
|
|
706
712
|
export interface BackupConfigs {
|
|
707
713
|
passwordStorage?: PasswordConfig
|
package/types.d.ts
CHANGED
|
@@ -115,6 +115,11 @@ export type MessageData =
|
|
|
115
115
|
export type ProgressCallback = (status: MpcStatus) => void | Promise<void>
|
|
116
116
|
export type ValidRpcErrorCodes = 4001 | 4100 | 4200 | 4900 | 4901
|
|
117
117
|
|
|
118
|
+
export interface FirebaseStorageConfigOptions {
|
|
119
|
+
getToken: (options?: { forceRefresh?: boolean }) => Promise<string | null>
|
|
120
|
+
tbsHost?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
118
123
|
// Interfaces
|
|
119
124
|
|
|
120
125
|
export interface BackupArgs extends MpcOperationArgs {
|