1
+ import { SafeWalletActionProvider } from "./safeWalletActionProvider" ;
2
+ import { SafeWalletProvider } from "../../wallet-providers" ;
3
+ import { AddSignerSchema } from "./schemas" ;
4
+ import Safe from "@safe-global/protocol-kit" ;
5
+
6
+ // Mock Safe SDK modules
7
+ jest . mock ( "@safe-global/protocol-kit" ) ;
8
+ jest . mock ( "@safe-global/api-kit" ) ;
9
+
10
+ describe ( "SafeWalletActionProvider" , ( ) => {
11
+ let actionProvider : SafeWalletActionProvider ;
12
+ let mockWallet : jest . Mocked < SafeWalletProvider > ;
13
+ const MOCK_SAFE_ADDRESS = "0x1234567890123456789012345678901234567890" ;
14
+ const MOCK_NEW_SIGNER = "0x9876543210987654321098765432109876543210" ;
15
+ const MOCK_TRANSACTION_HASH = "0xtxhash123" ;
16
+
17
+ beforeEach ( ( ) => {
18
+ // Reset mocks
19
+ jest . clearAllMocks ( ) ;
20
+
21
+ actionProvider = new SafeWalletActionProvider ( ) ;
22
+
23
+ // Mock SafeWalletProvider
24
+ mockWallet = {
25
+ getAddress : jest . fn ( ) . mockReturnValue ( MOCK_SAFE_ADDRESS ) ,
26
+ getNetwork : jest . fn ( ) . mockReturnValue ( { networkId : "base-sepolia" } ) ,
27
+ getSafeClient : jest . fn ( ) ,
28
+ waitForInitialization : jest . fn ( ) . mockResolvedValue ( undefined ) ,
29
+ addOwnerWithThreshold : jest . fn ( ) . mockResolvedValue (
30
+ `Successfully proposed adding signer ${ MOCK_NEW_SIGNER } to Safe ${ MOCK_SAFE_ADDRESS } . Safe transaction hash: ${ MOCK_TRANSACTION_HASH } . The other signers will need to confirm the transaction before it can be executed.`
31
+ ) ,
32
+ removeOwnerWithThreshold : jest . fn ( ) ,
33
+ changeThreshold : jest . fn ( ) ,
34
+ } as unknown as jest . Mocked < SafeWalletProvider > ;
35
+
36
+ // Mock Safe client methods
37
+ const mockSafeClient = {
38
+ getOwners : jest . fn ( ) . mockResolvedValue ( [ "0xowner1" , "0xowner2" ] ) ,
39
+ getThreshold : jest . fn ( ) . mockResolvedValue ( 2 ) ,
40
+ createTransaction : jest . fn ( ) . mockResolvedValue ( {
41
+ data : { safeTxHash : MOCK_TRANSACTION_HASH } ,
42
+ } ) ,
43
+ getPendingTransactions : jest . fn ( ) . mockResolvedValue ( {
44
+ results : [ ] ,
45
+ } ) ,
46
+ } as unknown as Safe ;
47
+
48
+ mockWallet . getSafeClient . mockReturnValue ( mockSafeClient ) ;
49
+ } ) ;
50
+
51
+ describe ( "Input Schema Validation" , ( ) => {
52
+ it ( "should validate AddSignerSchema with valid input" , ( ) => {
53
+ const validInput = {
54
+ safeAddress : MOCK_SAFE_ADDRESS ,
55
+ newSigner : MOCK_NEW_SIGNER ,
56
+ } ;
57
+
58
+ const result = AddSignerSchema . safeParse ( validInput ) ;
59
+ expect ( result . success ) . toBe ( true ) ;
60
+ } ) ;
61
+
62
+ it ( "should reject AddSignerSchema with invalid address" , ( ) => {
63
+ const invalidInput = {
64
+ safeAddress : "not-an-address" ,
65
+ newSigner : "not-an-address" ,
66
+ } ;
67
+
68
+ const result = AddSignerSchema . safeParse ( invalidInput ) ;
69
+ expect ( result . success ) . toBe ( false ) ;
70
+ } ) ;
71
+ } ) ;
72
+
73
+ describe ( "addSigner" , ( ) => {
74
+ it ( "should successfully add a new signer" , async ( ) => {
75
+ const args = {
76
+ safeAddress : MOCK_SAFE_ADDRESS ,
77
+ newSigner : MOCK_NEW_SIGNER ,
78
+ } ;
79
+
80
+ const response = await actionProvider . addSigner ( mockWallet , args ) ;
81
+
82
+ expect ( response ) . toContain ( `Successfully proposed adding signer ${ MOCK_NEW_SIGNER } ` ) ;
83
+ expect ( response ) . toContain ( `Safe transaction hash: ${ MOCK_TRANSACTION_HASH } ` ) ;
84
+ } ) ;
85
+
86
+ it ( "should fail when adding an existing owner" , async ( ) => {
87
+ const args = {
88
+ safeAddress : MOCK_SAFE_ADDRESS ,
89
+ newSigner : "0xowner1" , // Using an address that's already an owner
90
+ } ;
91
+
92
+ const error = new Error ( "Address is already an owner of this Safe" ) ;
93
+ mockWallet . addOwnerWithThreshold . mockRejectedValue ( error ) ;
94
+
95
+ await expect ( actionProvider . addSigner ( mockWallet , args ) ) . rejects . toThrow (
96
+ "Failed to add signer: Address is already an owner of this Safe"
97
+ ) ;
98
+ } ) ;
99
+
100
+ it ( "should fail when threshold is less than 1" , async ( ) => {
101
+ const args = {
102
+ safeAddress : MOCK_SAFE_ADDRESS ,
103
+ newSigner : MOCK_NEW_SIGNER ,
104
+ newThreshold : 0 ,
105
+ } ;
106
+
107
+ const error = new Error ( "Threshold must be at least 1" ) ;
108
+ mockWallet . addOwnerWithThreshold . mockRejectedValue ( error ) ;
109
+
110
+ await expect ( actionProvider . addSigner ( mockWallet , args ) ) . rejects . toThrow (
111
+ "Failed to add signer: Threshold must be at least 1"
112
+ ) ;
113
+ } ) ;
114
+
115
+ it ( "should fail when threshold is greater than owner count" , async ( ) => {
116
+ const args = {
117
+ safeAddress : MOCK_SAFE_ADDRESS ,
118
+ newSigner : MOCK_NEW_SIGNER ,
119
+ newThreshold : 4 , // Would be 3 owners after adding new signer
120
+ } ;
121
+
122
+ const error = new Error ( "Invalid threshold: 4 cannot be greater than number of owners (3)" ) ;
123
+ mockWallet . addOwnerWithThreshold . mockRejectedValue ( error ) ;
124
+
125
+ await expect ( actionProvider . addSigner ( mockWallet , args ) ) . rejects . toThrow (
126
+ "Failed to add signer: Invalid threshold: 4 cannot be greater than number of owners (3)"
127
+ ) ;
128
+ } ) ;
129
+ } ) ;
130
+
131
+ describe ( "removeSigner" , ( ) => {
132
+ it ( "should successfully remove a signer" , async ( ) => {
133
+ const args = {
134
+ safeAddress : MOCK_SAFE_ADDRESS ,
135
+ signerToRemove : "0xowner2" ,
136
+ newThreshold : 1 ,
137
+ } ;
138
+
139
+ mockWallet . removeOwnerWithThreshold = jest . fn ( ) . mockResolvedValue (
140
+ `Successfully proposed removing signer ${ args . signerToRemove } from Safe ${ MOCK_SAFE_ADDRESS } . Safe transaction hash: ${ MOCK_TRANSACTION_HASH } . The other signers will need to confirm the transaction before it can be executed.`
141
+ ) ;
142
+
143
+ const response = await actionProvider . removeSigner ( mockWallet , args ) ;
144
+
145
+ expect ( response ) . toContain ( `Successfully proposed removing signer ${ args . signerToRemove } ` ) ;
146
+ expect ( response ) . toContain ( `Safe transaction hash: ${ MOCK_TRANSACTION_HASH } ` ) ;
147
+ } ) ;
148
+
149
+ it ( "should fail when removing non-existent owner" , async ( ) => {
150
+ const args = {
151
+ safeAddress : MOCK_SAFE_ADDRESS ,
152
+ signerToRemove : MOCK_NEW_SIGNER ,
153
+ newThreshold : 1 ,
154
+ } ;
155
+
156
+ const error = new Error ( "Address is not an owner of this Safe" ) ;
157
+ mockWallet . removeOwnerWithThreshold = jest . fn ( ) . mockRejectedValue ( error ) ;
158
+
159
+ await expect ( actionProvider . removeSigner ( mockWallet , args ) ) . rejects . toThrow (
160
+ "Address is not an owner of this Safe"
161
+ ) ;
162
+ } ) ;
163
+ } ) ;
164
+
165
+ describe ( "changeThreshold" , ( ) => {
166
+ it ( "should successfully change threshold" , async ( ) => {
167
+ const args = {
168
+ safeAddress : MOCK_SAFE_ADDRESS ,
169
+ newThreshold : 2 ,
170
+ } ;
171
+
172
+ mockWallet . changeThreshold = jest . fn ( ) . mockResolvedValue (
173
+ `Successfully proposed changing threshold to ${ args . newThreshold } for Safe ${ MOCK_SAFE_ADDRESS } . Safe transaction hash: ${ MOCK_TRANSACTION_HASH } . The other signers will need to confirm the transaction before it can be executed.`
174
+ ) ;
175
+
176
+ const response = await actionProvider . changeThreshold ( mockWallet , args ) ;
177
+
178
+ expect ( response ) . toContain ( `Successfully proposed changing threshold to ${ args . newThreshold } ` ) ;
179
+ expect ( response ) . toContain ( `Safe transaction hash: ${ MOCK_TRANSACTION_HASH } ` ) ;
180
+ } ) ;
181
+
182
+ it ( "should fail when threshold is invalid" , async ( ) => {
183
+ const args = {
184
+ safeAddress : MOCK_SAFE_ADDRESS ,
185
+ newThreshold : 3 ,
186
+ } ;
187
+
188
+ const error = new Error ( "Threshold cannot be greater than owners length" ) ;
189
+ mockWallet . changeThreshold = jest . fn ( ) . mockRejectedValue ( error ) ;
190
+
191
+ await expect ( actionProvider . changeThreshold ( mockWallet , args ) ) . rejects . toThrow (
192
+ "Threshold cannot be greater than owners length"
193
+ ) ;
194
+ } ) ;
195
+ } ) ;
196
+
197
+ describe ( "supportsNetwork" , ( ) => {
198
+ it ( "should return true for EVM networks" , ( ) => {
199
+ const evmNetwork = { protocolFamily : "evm" , networkId : "base-sepolia" , chainId : "1" } ;
200
+ expect ( actionProvider . supportsNetwork ( evmNetwork ) ) . toBe ( true ) ;
201
+ } ) ;
202
+
203
+ it ( "should return false for non-EVM networks" , ( ) => {
204
+ const nonEvmNetwork = { protocolFamily : "svm" , networkId : "solana" , chainId : "1" } ;
205
+ expect ( actionProvider . supportsNetwork ( nonEvmNetwork ) ) . toBe ( false ) ;
206
+ } ) ;
207
+ } ) ;
208
+ } ) ;
0 commit comments