@portal-hq/web 3.17.0 → 3.18.0

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.
Files changed (55) hide show
  1. package/lib/commonjs/index.js +62 -20
  2. package/lib/commonjs/index.test.js +51 -10
  3. package/lib/commonjs/integrations/ramps/index.js +2 -0
  4. package/lib/commonjs/integrations/ramps/meld/index.js +121 -0
  5. package/lib/commonjs/integrations/ramps/meld/index.test.js +162 -0
  6. package/lib/commonjs/integrations/ramps/noah/index.js +2 -2
  7. package/lib/commonjs/integrations/ramps/noah/index.test.js +11 -2
  8. package/lib/commonjs/mpc/index.js +199 -3
  9. package/lib/commonjs/mpc/index.test.js +634 -0
  10. package/lib/commonjs/provider/index.js +44 -2
  11. package/lib/commonjs/provider/index.test.js +186 -0
  12. package/lib/commonjs/rpc.test.js +162 -0
  13. package/lib/commonjs/shared/rpc/auth.js +29 -0
  14. package/lib/commonjs/shared/rpc/defaults.js +40 -0
  15. package/lib/commonjs/shared/types/index.js +1 -0
  16. package/lib/commonjs/shared/types/meld.js +2 -0
  17. package/lib/esm/index.js +59 -19
  18. package/lib/esm/index.test.js +51 -10
  19. package/lib/esm/integrations/ramps/index.js +2 -0
  20. package/lib/esm/integrations/ramps/meld/index.js +118 -0
  21. package/lib/esm/integrations/ramps/meld/index.test.js +157 -0
  22. package/lib/esm/integrations/ramps/noah/index.js +2 -2
  23. package/lib/esm/integrations/ramps/noah/index.test.js +11 -2
  24. package/lib/esm/mpc/index.js +199 -3
  25. package/lib/esm/mpc/index.test.js +635 -1
  26. package/lib/esm/provider/index.js +44 -2
  27. package/lib/esm/provider/index.test.js +186 -0
  28. package/lib/esm/rpc.test.js +157 -0
  29. package/lib/esm/shared/rpc/auth.js +25 -0
  30. package/lib/esm/shared/rpc/defaults.js +36 -0
  31. package/lib/esm/shared/types/index.js +1 -0
  32. package/lib/esm/shared/types/meld.js +1 -0
  33. package/noah-types.d.ts +17 -1
  34. package/package.json +1 -1
  35. package/src/__mocks/constants.ts +68 -0
  36. package/src/__mocks/portal/portal.ts +0 -1
  37. package/src/index.test.ts +90 -0
  38. package/src/index.ts +159 -7
  39. package/src/integrations/ramps/index.ts +3 -0
  40. package/src/integrations/ramps/meld/index.test.ts +189 -0
  41. package/src/integrations/ramps/meld/index.ts +149 -0
  42. package/src/integrations/ramps/noah/index.test.ts +11 -2
  43. package/src/integrations/ramps/noah/index.ts +5 -2
  44. package/src/mpc/index.test.ts +804 -7
  45. package/src/mpc/index.ts +247 -3
  46. package/src/provider/index.test.ts +233 -0
  47. package/src/provider/index.ts +64 -1
  48. package/src/rpc.test.ts +190 -0
  49. package/src/shared/rpc/auth.ts +27 -0
  50. package/src/shared/rpc/defaults.ts +41 -0
  51. package/src/shared/types/common.ts +24 -0
  52. package/src/shared/types/index.ts +1 -0
  53. package/src/shared/types/meld.ts +302 -0
  54. package/src/shared/types/noah.ts +191 -29
  55. package/types.d.ts +65 -3
@@ -80,6 +80,17 @@ import {
80
80
  mockBlockaidScanTokensResponse,
81
81
  mockBlockaidScanUrlRequest,
82
82
  mockBlockaidScanUrlResponse,
83
+ mockMeldCreateCustomerRequest,
84
+ mockMeldCreateCustomerResponse,
85
+ mockMeldSearchCustomerResponse,
86
+ mockMeldGetRetailQuoteRequest,
87
+ mockMeldGetRetailQuoteResponse,
88
+ mockMeldCreateRetailWidgetRequest,
89
+ mockMeldCreateRetailWidgetResponse,
90
+ mockMeldSearchRetailTransactionsResponse,
91
+ mockMeldGetRetailTransactionResponse,
92
+ mockMeldDiscoveryResponse,
93
+ mockMeldDiscoveryParams,
83
94
  mockBuildBatchedUserOpRequest,
84
95
  mockBuildBatchedUserOpResponse,
85
96
  mockBroadcastBatchedUserOpRequest,
@@ -4452,6 +4463,606 @@ describe('Mpc', () => {
4452
4463
  })
4453
4464
  })
4454
4465
 
4466
+ describe('meldCreateCustomer', () => {
4467
+ const args = mockMeldCreateCustomerRequest
4468
+ const res = mockMeldCreateCustomerResponse
4469
+
4470
+ it('should successfully return the created customer', (done) => {
4471
+ jest
4472
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4473
+ .mockImplementation((message: any, origin?) => {
4474
+ const { type, data } = message
4475
+
4476
+ expect(type).toEqual('portal:meld:createCustomer')
4477
+ expect(data).toMatchObject(args)
4478
+ expect(typeof message.traceId).toBe('string')
4479
+ expect(origin).toEqual(mockHostOrigin)
4480
+
4481
+ window.dispatchEvent(
4482
+ new MessageEvent('message', {
4483
+ origin: mockHostOrigin,
4484
+ data: {
4485
+ type: 'portal:meld:createCustomerResult',
4486
+ data: res,
4487
+ },
4488
+ }),
4489
+ )
4490
+ })
4491
+
4492
+ mpc
4493
+ .meldCreateCustomer(args)
4494
+ .then((data) => {
4495
+ expect(data).toEqual(res)
4496
+ done()
4497
+ })
4498
+ .catch((_) => {
4499
+ expect(0).toEqual(1)
4500
+ done()
4501
+ })
4502
+ })
4503
+
4504
+ it('should error out if the iframe sends an error message', (done) => {
4505
+ jest
4506
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4507
+ .mockImplementationOnce((message: any, origin?) => {
4508
+ const { type } = message
4509
+
4510
+ expect(type).toEqual('portal:meld:createCustomer')
4511
+ expect(origin).toEqual(mockHostOrigin)
4512
+
4513
+ window.dispatchEvent(
4514
+ new MessageEvent('message', {
4515
+ origin: mockHostOrigin,
4516
+ data: {
4517
+ type: 'portal:meld:createCustomerError',
4518
+ data: { code: 1, message: 'test' },
4519
+ },
4520
+ }),
4521
+ )
4522
+ })
4523
+
4524
+ mpc
4525
+ .meldCreateCustomer(args)
4526
+ .then(() => {
4527
+ expect(0).toEqual(1)
4528
+ done()
4529
+ })
4530
+ .catch((e) => {
4531
+ expect(e).toBeInstanceOf(PortalMpcError)
4532
+ expect(e.message).toEqual('test')
4533
+ expect(e.code).toEqual(1)
4534
+ done()
4535
+ })
4536
+ })
4537
+ })
4538
+
4539
+ describe('meldSearchCustomer', () => {
4540
+ const res = mockMeldSearchCustomerResponse
4541
+
4542
+ it('should successfully return the customer data', (done) => {
4543
+ jest
4544
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4545
+ .mockImplementation((message: any, origin?) => {
4546
+ const { type } = message
4547
+
4548
+ expect(type).toEqual('portal:meld:searchCustomer')
4549
+ expect(origin).toEqual(mockHostOrigin)
4550
+
4551
+ window.dispatchEvent(
4552
+ new MessageEvent('message', {
4553
+ origin: mockHostOrigin,
4554
+ data: {
4555
+ type: 'portal:meld:searchCustomerResult',
4556
+ data: res,
4557
+ },
4558
+ }),
4559
+ )
4560
+ })
4561
+
4562
+ mpc
4563
+ .meldSearchCustomer()
4564
+ .then((data) => {
4565
+ expect(data).toEqual(res)
4566
+ done()
4567
+ })
4568
+ .catch((_) => {
4569
+ expect(0).toEqual(1)
4570
+ done()
4571
+ })
4572
+ })
4573
+
4574
+ it('should error out if the iframe sends an error message', (done) => {
4575
+ jest
4576
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4577
+ .mockImplementationOnce((_message: any, _origin?) => {
4578
+ window.dispatchEvent(
4579
+ new MessageEvent('message', {
4580
+ origin: mockHostOrigin,
4581
+ data: {
4582
+ type: 'portal:meld:searchCustomerError',
4583
+ data: { code: 2, message: 'not found' },
4584
+ },
4585
+ }),
4586
+ )
4587
+ })
4588
+
4589
+ mpc
4590
+ .meldSearchCustomer()
4591
+ .then(() => {
4592
+ expect(0).toEqual(1)
4593
+ done()
4594
+ })
4595
+ .catch((e) => {
4596
+ expect(e).toBeInstanceOf(PortalMpcError)
4597
+ expect(e.code).toEqual(2)
4598
+ done()
4599
+ })
4600
+ })
4601
+ })
4602
+
4603
+ describe('meldGetRetailQuote', () => {
4604
+ const args = mockMeldGetRetailQuoteRequest
4605
+ const res = mockMeldGetRetailQuoteResponse
4606
+
4607
+ it('should successfully return the retail quote', (done) => {
4608
+ jest
4609
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4610
+ .mockImplementation((message: any, origin?) => {
4611
+ const { type, data } = message
4612
+
4613
+ expect(type).toEqual('portal:meld:getRetailQuote')
4614
+ expect(data).toMatchObject(args)
4615
+ expect(typeof message.traceId).toBe('string')
4616
+ expect(origin).toEqual(mockHostOrigin)
4617
+
4618
+ window.dispatchEvent(
4619
+ new MessageEvent('message', {
4620
+ origin: mockHostOrigin,
4621
+ data: {
4622
+ type: 'portal:meld:getRetailQuoteResult',
4623
+ data: res,
4624
+ },
4625
+ }),
4626
+ )
4627
+ })
4628
+
4629
+ mpc
4630
+ .meldGetRetailQuote(args)
4631
+ .then((data) => {
4632
+ expect(data).toEqual(res)
4633
+ done()
4634
+ })
4635
+ .catch((_) => {
4636
+ expect(0).toEqual(1)
4637
+ done()
4638
+ })
4639
+ })
4640
+
4641
+ it('should error out if the iframe sends an error message', (done) => {
4642
+ jest
4643
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4644
+ .mockImplementationOnce((_message: any, _origin?) => {
4645
+ window.dispatchEvent(
4646
+ new MessageEvent('message', {
4647
+ origin: mockHostOrigin,
4648
+ data: {
4649
+ type: 'portal:meld:getRetailQuoteError',
4650
+ data: { code: 1, message: 'test' },
4651
+ },
4652
+ }),
4653
+ )
4654
+ })
4655
+
4656
+ mpc
4657
+ .meldGetRetailQuote(args)
4658
+ .then(() => {
4659
+ expect(0).toEqual(1)
4660
+ done()
4661
+ })
4662
+ .catch((e) => {
4663
+ expect(e).toBeInstanceOf(PortalMpcError)
4664
+ expect(e.message).toEqual('test')
4665
+ expect(e.code).toEqual(1)
4666
+ done()
4667
+ })
4668
+ })
4669
+ })
4670
+
4671
+ describe('meldCreateRetailWidget', () => {
4672
+ const args = mockMeldCreateRetailWidgetRequest
4673
+ const res = mockMeldCreateRetailWidgetResponse
4674
+
4675
+ it('should successfully return the retail widget', (done) => {
4676
+ jest
4677
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4678
+ .mockImplementation((message: any, origin?) => {
4679
+ expect(message.type).toEqual('portal:meld:createRetailWidget')
4680
+ expect(message.data).toMatchObject(args)
4681
+ expect(origin).toEqual(mockHostOrigin)
4682
+
4683
+ window.dispatchEvent(
4684
+ new MessageEvent('message', {
4685
+ origin: mockHostOrigin,
4686
+ data: { type: 'portal:meld:createRetailWidgetResult', data: res },
4687
+ }),
4688
+ )
4689
+ })
4690
+
4691
+ mpc
4692
+ .meldCreateRetailWidget(args)
4693
+ .then((data) => {
4694
+ expect(data).toEqual(res)
4695
+ done()
4696
+ })
4697
+ .catch((_) => {
4698
+ expect(0).toEqual(1)
4699
+ done()
4700
+ })
4701
+ })
4702
+ })
4703
+
4704
+ describe('meldSearchRetailTransactions', () => {
4705
+ const res = mockMeldSearchRetailTransactionsResponse
4706
+
4707
+ it('should successfully return transactions without params', (done) => {
4708
+ jest
4709
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4710
+ .mockImplementation((message: any, origin?) => {
4711
+ expect(message.type).toEqual('portal:meld:searchRetailTransactions')
4712
+ expect(origin).toEqual(mockHostOrigin)
4713
+
4714
+ window.dispatchEvent(
4715
+ new MessageEvent('message', {
4716
+ origin: mockHostOrigin,
4717
+ data: { type: 'portal:meld:searchRetailTransactionsResult', data: res },
4718
+ }),
4719
+ )
4720
+ })
4721
+
4722
+ mpc
4723
+ .meldSearchRetailTransactions()
4724
+ .then((data) => {
4725
+ expect(data).toEqual(res)
4726
+ done()
4727
+ })
4728
+ .catch((_) => {
4729
+ expect(0).toEqual(1)
4730
+ done()
4731
+ })
4732
+ })
4733
+ })
4734
+
4735
+ describe('meldGetRetailTransactionBySession', () => {
4736
+ const res = mockMeldGetRetailTransactionResponse
4737
+
4738
+ it('should successfully return a transaction by session ID', (done) => {
4739
+ jest
4740
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4741
+ .mockImplementation((message: any, origin?) => {
4742
+ expect(message.type).toEqual('portal:meld:getRetailTransactionBySession')
4743
+ expect(message.data).toEqual('ses-123')
4744
+ expect(origin).toEqual(mockHostOrigin)
4745
+
4746
+ window.dispatchEvent(
4747
+ new MessageEvent('message', {
4748
+ origin: mockHostOrigin,
4749
+ data: { type: 'portal:meld:getRetailTransactionBySessionResult', data: res },
4750
+ }),
4751
+ )
4752
+ })
4753
+
4754
+ mpc
4755
+ .meldGetRetailTransactionBySession('ses-123')
4756
+ .then((data) => {
4757
+ expect(data).toEqual(res)
4758
+ done()
4759
+ })
4760
+ .catch((_) => {
4761
+ expect(0).toEqual(1)
4762
+ done()
4763
+ })
4764
+ })
4765
+ })
4766
+
4767
+ describe('meldGetRetailTransaction', () => {
4768
+ const res = mockMeldGetRetailTransactionResponse
4769
+
4770
+ it('should successfully return a transaction by ID', (done) => {
4771
+ jest
4772
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4773
+ .mockImplementation((message: any, origin?) => {
4774
+ expect(message.type).toEqual('portal:meld:getRetailTransaction')
4775
+ expect(message.data).toEqual('tx-123')
4776
+ expect(origin).toEqual(mockHostOrigin)
4777
+
4778
+ window.dispatchEvent(
4779
+ new MessageEvent('message', {
4780
+ origin: mockHostOrigin,
4781
+ data: { type: 'portal:meld:getRetailTransactionResult', data: res },
4782
+ }),
4783
+ )
4784
+ })
4785
+
4786
+ mpc
4787
+ .meldGetRetailTransaction('tx-123')
4788
+ .then((data) => {
4789
+ expect(data).toEqual(res)
4790
+ done()
4791
+ })
4792
+ .catch((_) => {
4793
+ expect(0).toEqual(1)
4794
+ done()
4795
+ })
4796
+ })
4797
+ })
4798
+
4799
+ describe('meldGetServiceProviders', () => {
4800
+ const res = mockMeldDiscoveryResponse
4801
+
4802
+ it('should successfully return service providers without params', (done) => {
4803
+ jest
4804
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4805
+ .mockImplementation((message: any, origin?) => {
4806
+ expect(message.type).toEqual('portal:meld:getServiceProviders')
4807
+ expect(origin).toEqual(mockHostOrigin)
4808
+
4809
+ window.dispatchEvent(
4810
+ new MessageEvent('message', {
4811
+ origin: mockHostOrigin,
4812
+ data: { type: 'portal:meld:getServiceProvidersResult', data: res },
4813
+ }),
4814
+ )
4815
+ })
4816
+
4817
+ mpc
4818
+ .meldGetServiceProviders()
4819
+ .then((data) => {
4820
+ expect(data).toEqual(res)
4821
+ done()
4822
+ })
4823
+ .catch((_) => {
4824
+ expect(0).toEqual(1)
4825
+ done()
4826
+ })
4827
+ })
4828
+
4829
+ it('should pass params to the iframe when provided', (done) => {
4830
+ const params = mockMeldDiscoveryParams
4831
+ jest
4832
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4833
+ .mockImplementation((message: any, origin?) => {
4834
+ expect(message.type).toEqual('portal:meld:getServiceProviders')
4835
+ expect(message.data).toMatchObject(params)
4836
+ expect(origin).toEqual(mockHostOrigin)
4837
+
4838
+ window.dispatchEvent(
4839
+ new MessageEvent('message', {
4840
+ origin: mockHostOrigin,
4841
+ data: { type: 'portal:meld:getServiceProvidersResult', data: res },
4842
+ }),
4843
+ )
4844
+ })
4845
+
4846
+ mpc
4847
+ .meldGetServiceProviders(params)
4848
+ .then(() => done())
4849
+ .catch((_) => {
4850
+ expect(0).toEqual(1)
4851
+ done()
4852
+ })
4853
+ })
4854
+ })
4855
+
4856
+ describe('meldGetCountries', () => {
4857
+ it('should successfully return countries', (done) => {
4858
+ const res = mockMeldDiscoveryResponse
4859
+ jest
4860
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4861
+ .mockImplementation((message: any, origin?) => {
4862
+ expect(message.type).toEqual('portal:meld:getCountries')
4863
+ expect(origin).toEqual(mockHostOrigin)
4864
+
4865
+ window.dispatchEvent(
4866
+ new MessageEvent('message', {
4867
+ origin: mockHostOrigin,
4868
+ data: { type: 'portal:meld:getCountriesResult', data: res },
4869
+ }),
4870
+ )
4871
+ })
4872
+
4873
+ mpc
4874
+ .meldGetCountries()
4875
+ .then((data) => {
4876
+ expect(data).toEqual(res)
4877
+ done()
4878
+ })
4879
+ .catch((_) => {
4880
+ expect(0).toEqual(1)
4881
+ done()
4882
+ })
4883
+ })
4884
+
4885
+ it('should error out if the iframe sends an error message', (done) => {
4886
+ jest
4887
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4888
+ .mockImplementationOnce((_message: any, _origin?) => {
4889
+ window.dispatchEvent(
4890
+ new MessageEvent('message', {
4891
+ origin: mockHostOrigin,
4892
+ data: { type: 'portal:meld:getCountriesError', data: { code: 1, message: 'test' } },
4893
+ }),
4894
+ )
4895
+ })
4896
+
4897
+ mpc
4898
+ .meldGetCountries()
4899
+ .then(() => {
4900
+ expect(0).toEqual(1)
4901
+ done()
4902
+ })
4903
+ .catch((e) => {
4904
+ expect(e).toBeInstanceOf(PortalMpcError)
4905
+ done()
4906
+ })
4907
+ })
4908
+ })
4909
+
4910
+ describe('meldGetFiatCurrencies', () => {
4911
+ it('should successfully return fiat currencies', (done) => {
4912
+ const res = mockMeldDiscoveryResponse
4913
+ jest
4914
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4915
+ .mockImplementation((message: any, origin?) => {
4916
+ expect(message.type).toEqual('portal:meld:getFiatCurrencies')
4917
+ expect(origin).toEqual(mockHostOrigin)
4918
+
4919
+ window.dispatchEvent(
4920
+ new MessageEvent('message', {
4921
+ origin: mockHostOrigin,
4922
+ data: { type: 'portal:meld:getFiatCurrenciesResult', data: res },
4923
+ }),
4924
+ )
4925
+ })
4926
+
4927
+ mpc
4928
+ .meldGetFiatCurrencies()
4929
+ .then((data) => {
4930
+ expect(data).toEqual(res)
4931
+ done()
4932
+ })
4933
+ .catch((_) => {
4934
+ expect(0).toEqual(1)
4935
+ done()
4936
+ })
4937
+ })
4938
+ })
4939
+
4940
+ describe('meldGetCryptoCurrencies', () => {
4941
+ it('should successfully return crypto currencies', (done) => {
4942
+ const res = mockMeldDiscoveryResponse
4943
+ jest
4944
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4945
+ .mockImplementation((message: any, origin?) => {
4946
+ expect(message.type).toEqual('portal:meld:getCryptoCurrencies')
4947
+ expect(origin).toEqual(mockHostOrigin)
4948
+
4949
+ window.dispatchEvent(
4950
+ new MessageEvent('message', {
4951
+ origin: mockHostOrigin,
4952
+ data: { type: 'portal:meld:getCryptoCurrenciesResult', data: res },
4953
+ }),
4954
+ )
4955
+ })
4956
+
4957
+ mpc.meldGetCryptoCurrencies().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
4958
+ })
4959
+ })
4960
+
4961
+ describe('meldGetPaymentMethods', () => {
4962
+ it('should successfully return payment methods', (done) => {
4963
+ const res = mockMeldDiscoveryResponse
4964
+ jest
4965
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4966
+ .mockImplementation((message: any, origin?) => {
4967
+ expect(message.type).toEqual('portal:meld:getPaymentMethods')
4968
+ expect(origin).toEqual(mockHostOrigin)
4969
+
4970
+ window.dispatchEvent(
4971
+ new MessageEvent('message', {
4972
+ origin: mockHostOrigin,
4973
+ data: { type: 'portal:meld:getPaymentMethodsResult', data: res },
4974
+ }),
4975
+ )
4976
+ })
4977
+
4978
+ mpc.meldGetPaymentMethods().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
4979
+ })
4980
+ })
4981
+
4982
+ describe('meldGetDefaults', () => {
4983
+ it('should successfully return defaults', (done) => {
4984
+ const res = mockMeldDiscoveryResponse
4985
+ jest
4986
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4987
+ .mockImplementation((message: any, origin?) => {
4988
+ expect(message.type).toEqual('portal:meld:getDefaults')
4989
+ expect(origin).toEqual(mockHostOrigin)
4990
+
4991
+ window.dispatchEvent(
4992
+ new MessageEvent('message', {
4993
+ origin: mockHostOrigin,
4994
+ data: { type: 'portal:meld:getDefaultsResult', data: res },
4995
+ }),
4996
+ )
4997
+ })
4998
+
4999
+ mpc.meldGetDefaults().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
5000
+ })
5001
+ })
5002
+
5003
+ describe('meldGetBuyLimits', () => {
5004
+ it('should successfully return buy limits', (done) => {
5005
+ const res = mockMeldDiscoveryResponse
5006
+ jest
5007
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
5008
+ .mockImplementation((message: any, origin?) => {
5009
+ expect(message.type).toEqual('portal:meld:getBuyLimits')
5010
+ expect(origin).toEqual(mockHostOrigin)
5011
+
5012
+ window.dispatchEvent(
5013
+ new MessageEvent('message', {
5014
+ origin: mockHostOrigin,
5015
+ data: { type: 'portal:meld:getBuyLimitsResult', data: res },
5016
+ }),
5017
+ )
5018
+ })
5019
+
5020
+ mpc.meldGetBuyLimits().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
5021
+ })
5022
+ })
5023
+
5024
+ describe('meldGetSellLimits', () => {
5025
+ it('should successfully return sell limits', (done) => {
5026
+ const res = mockMeldDiscoveryResponse
5027
+ jest
5028
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
5029
+ .mockImplementation((message: any, origin?) => {
5030
+ expect(message.type).toEqual('portal:meld:getSellLimits')
5031
+ expect(origin).toEqual(mockHostOrigin)
5032
+
5033
+ window.dispatchEvent(
5034
+ new MessageEvent('message', {
5035
+ origin: mockHostOrigin,
5036
+ data: { type: 'portal:meld:getSellLimitsResult', data: res },
5037
+ }),
5038
+ )
5039
+ })
5040
+
5041
+ mpc.meldGetSellLimits().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
5042
+ })
5043
+ })
5044
+
5045
+ describe('meldGetKycLimits', () => {
5046
+ it('should successfully return KYC limits', (done) => {
5047
+ const res = mockMeldDiscoveryResponse
5048
+ jest
5049
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
5050
+ .mockImplementation((message: any, origin?) => {
5051
+ expect(message.type).toEqual('portal:meld:getKycLimits')
5052
+ expect(origin).toEqual(mockHostOrigin)
5053
+
5054
+ window.dispatchEvent(
5055
+ new MessageEvent('message', {
5056
+ origin: mockHostOrigin,
5057
+ data: { type: 'portal:meld:getKycLimitsResult', data: res },
5058
+ }),
5059
+ )
5060
+ })
5061
+
5062
+ mpc.meldGetKycLimits().then((data) => { expect(data).toEqual(res); done() }).catch((_) => { expect(0).toEqual(1); done() })
5063
+ })
5064
+ })
5065
+
4455
5066
  describe('accountAbstractionBuildBatchedUserOp', () => {
4456
5067
  const args = mockBuildBatchedUserOpRequest
4457
5068
  const res = mockBuildBatchedUserOpResponse
@@ -4540,9 +5151,7 @@ describe('Mpc', () => {
4540
5151
  .mockImplementation((message: any, origin?) => {
4541
5152
  const { type, data } = message
4542
5153
 
4543
- expect(type).toEqual(
4544
- 'portal:accountAbstraction:broadcastBatchedUserOp',
4545
- )
5154
+ expect(type).toEqual('portal:accountAbstraction:broadcastBatchedUserOp')
4546
5155
  expect(data).toMatchObject(args)
4547
5156
  expect(typeof message.traceId).toBe('string')
4548
5157
  expect(origin).toEqual(mockHostOrigin)
@@ -4576,9 +5185,7 @@ describe('Mpc', () => {
4576
5185
  .mockImplementationOnce((message: any, origin?) => {
4577
5186
  const { type, data } = message
4578
5187
 
4579
- expect(type).toEqual(
4580
- 'portal:accountAbstraction:broadcastBatchedUserOp',
4581
- )
5188
+ expect(type).toEqual('portal:accountAbstraction:broadcastBatchedUserOp')
4582
5189
  expect(data).toMatchObject(args)
4583
5190
  expect(typeof message.traceId).toBe('string')
4584
5191
  expect(origin).toEqual(mockHostOrigin)
@@ -4611,4 +5218,194 @@ describe('Mpc', () => {
4611
5218
  })
4612
5219
  })
4613
5220
  })
4614
- })
5221
+
5222
+
5223
+ describe('walletNotOnDevice detection', () => {
5224
+ const makeClient = (overrides: Record<string, any> = {}) => ({
5225
+ id: 'client-test',
5226
+ address: mockAddress,
5227
+ ejectedAt: null as string | null,
5228
+ custodian: { id: 'c', name: 'c' },
5229
+ environment: { id: 'e', name: 'e', backupWithPortalEnabled: false, isMultiBackupEnabled: false },
5230
+ isAccountAbstracted: false,
5231
+ metadata: { namespaces: {} },
5232
+ wallets: [] as any[],
5233
+ ...overrides,
5234
+ })
5235
+
5236
+ const makeWallet = (
5237
+ curve: PortalCurve,
5238
+ signingStatus: string,
5239
+ backupStatus: string,
5240
+ ) => ({
5241
+ id: `wallet-${curve}`,
5242
+ createdAt: '2024-01-01T00:00:00.000Z',
5243
+ curve,
5244
+ publicKey: 'pk',
5245
+ signingSharePairs: [{ id: 'sp1', createdAt: '2024-01-01T00:00:00.000Z', status: signingStatus }],
5246
+ backupSharePairs: [
5247
+ {
5248
+ id: 'bp1',
5249
+ createdAt: '2024-01-01T00:00:00.000Z',
5250
+ backupMethod: BackupMethods.password,
5251
+ status: backupStatus,
5252
+ },
5253
+ ],
5254
+ })
5255
+
5256
+ const dispatchReady = () => {
5257
+ window.dispatchEvent(
5258
+ new MessageEvent('message', {
5259
+ origin: `https://${mockHost}`,
5260
+ data: { type: 'portal:ready', data: true },
5261
+ }),
5262
+ )
5263
+ }
5264
+
5265
+ const flushPromises = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
5266
+
5267
+ beforeEach(() => {
5268
+ jest.spyOn(mpc, 'getAddress').mockResolvedValue(mockAddress)
5269
+ ;(mpc as any).waitForReadyMessage()
5270
+ })
5271
+
5272
+ it('does not emit when client has no wallets (new user / initial setup)', async () => {
5273
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(makeClient({ wallets: [] }) as any)
5274
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5275
+
5276
+ dispatchReady()
5277
+ await flushPromises()
5278
+
5279
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5280
+ expect(portalMock.triggerReady).toHaveBeenCalled()
5281
+ })
5282
+
5283
+ it('does not emit when wallets exist but all signing share pairs are incomplete (setup in progress)', async () => {
5284
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5285
+ makeClient({
5286
+ wallets: [
5287
+ makeWallet(PortalCurve.SECP256K1, 'incomplete', 'incomplete'),
5288
+ makeWallet(PortalCurve.ED25519, 'incomplete', 'incomplete'),
5289
+ ],
5290
+ }) as any,
5291
+ )
5292
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5293
+
5294
+ dispatchReady()
5295
+ await flushPromises()
5296
+
5297
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5298
+ expect(portalMock.triggerReady).toHaveBeenCalled()
5299
+ })
5300
+
5301
+ it('does not emit when wallet is present on device', async () => {
5302
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5303
+ makeClient({
5304
+ wallets: [
5305
+ makeWallet(PortalCurve.SECP256K1, 'completed', 'completed'),
5306
+ makeWallet(PortalCurve.ED25519, 'completed', 'completed'),
5307
+ ],
5308
+ }) as any,
5309
+ )
5310
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: true, ED25519: true })
5311
+
5312
+ dispatchReady()
5313
+ await flushPromises()
5314
+
5315
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5316
+ })
5317
+
5318
+ it('emits walletNotOnDevice with isBackedUp=false when wallet is not on device and not backed up', async () => {
5319
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5320
+ makeClient({
5321
+ wallets: [
5322
+ makeWallet(PortalCurve.SECP256K1, 'completed', 'incomplete'),
5323
+ makeWallet(PortalCurve.ED25519, 'completed', 'incomplete'),
5324
+ ],
5325
+ }) as any,
5326
+ )
5327
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5328
+
5329
+ dispatchReady()
5330
+ await flushPromises()
5331
+
5332
+ expect(portalMock.triggerWalletNotOnDevice).toHaveBeenCalledWith({
5333
+ clientId: 'client-test',
5334
+ isBackedUp: false,
5335
+ reason: 'storage_cleared',
5336
+ })
5337
+ })
5338
+
5339
+ it('emits walletNotOnDevice with isBackedUp=true when at least one backup is complete', async () => {
5340
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5341
+ makeClient({
5342
+ wallets: [makeWallet(PortalCurve.SECP256K1, 'completed', 'completed')],
5343
+ }) as any,
5344
+ )
5345
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5346
+
5347
+ dispatchReady()
5348
+ await flushPromises()
5349
+
5350
+ expect(portalMock.triggerWalletNotOnDevice).toHaveBeenCalledWith({
5351
+ clientId: 'client-test',
5352
+ isBackedUp: true,
5353
+ reason: 'storage_cleared',
5354
+ })
5355
+ })
5356
+
5357
+ it('does not emit for EVM-only client when SECP256K1 share is present (ED25519 absence is irrelevant)', async () => {
5358
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5359
+ makeClient({
5360
+ wallets: [makeWallet(PortalCurve.SECP256K1, 'completed', 'incomplete')],
5361
+ }) as any,
5362
+ )
5363
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: true, ED25519: false })
5364
+
5365
+ dispatchReady()
5366
+ await flushPromises()
5367
+
5368
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5369
+ })
5370
+
5371
+ it('emits for EVM-only client when SECP256K1 share is missing', async () => {
5372
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5373
+ makeClient({
5374
+ wallets: [makeWallet(PortalCurve.SECP256K1, 'completed', 'incomplete')],
5375
+ }) as any,
5376
+ )
5377
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5378
+
5379
+ dispatchReady()
5380
+ await flushPromises()
5381
+
5382
+ expect(portalMock.triggerWalletNotOnDevice).toHaveBeenCalled()
5383
+ })
5384
+
5385
+ it('does not emit when wallet is ejected (recovery is impossible)', async () => {
5386
+ jest.spyOn(mpc, 'getClient').mockResolvedValue(
5387
+ makeClient({
5388
+ ejectedAt: '2024-06-01T00:00:00.000Z',
5389
+ wallets: [makeWallet(PortalCurve.SECP256K1, 'completed', 'incomplete')],
5390
+ }) as any,
5391
+ )
5392
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockResolvedValue({ SECP256K1: false, ED25519: false })
5393
+
5394
+ dispatchReady()
5395
+ await flushPromises()
5396
+
5397
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5398
+ })
5399
+
5400
+ it('still calls triggerReady when detection throws (detection must not block initialization)', async () => {
5401
+ jest.spyOn(mpc, 'getClient').mockRejectedValue(new Error('network error'))
5402
+ jest.spyOn(mpc, 'checkSharesOnDevice').mockRejectedValue(new Error('network error'))
5403
+
5404
+ dispatchReady()
5405
+ await flushPromises()
5406
+
5407
+ expect(portalMock.triggerWalletNotOnDevice).not.toHaveBeenCalled()
5408
+ expect(portalMock.triggerReady).toHaveBeenCalled()
5409
+ })
5410
+ })
5411
+ })