@hub3js/std 0.0.1-next.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.
- package/actions/package.json +8 -0
- package/builders/package.json +8 -0
- package/dist/actions/disconnect.d.ts +8 -0
- package/dist/actions/disconnect.d.ts.map +1 -0
- package/dist/actions/mod.d.ts +2 -0
- package/dist/actions/mod.d.ts.map +1 -0
- package/dist/actions/mod.js +2 -0
- package/dist/actions/mod.js.map +7 -0
- package/dist/builders/disconnect.d.ts +10 -0
- package/dist/builders/disconnect.d.ts.map +1 -0
- package/dist/builders/mod.d.ts +2 -0
- package/dist/builders/mod.d.ts.map +1 -0
- package/dist/builders/mod.js +2 -0
- package/dist/builders/mod.js.map +7 -0
- package/dist/hooks/changeAccountSubscriber.d.ts +89 -0
- package/dist/hooks/changeAccountSubscriber.d.ts.map +1 -0
- package/dist/hooks/mod.d.ts +2 -0
- package/dist/hooks/mod.d.ts.map +1 -0
- package/dist/hooks/mod.js +2 -0
- package/dist/hooks/mod.js.map +7 -0
- package/dist/operators/after.d.ts +4 -0
- package/dist/operators/after.d.ts.map +1 -0
- package/dist/operators/and.d.ts +6 -0
- package/dist/operators/and.d.ts.map +1 -0
- package/dist/operators/before.d.ts +8 -0
- package/dist/operators/before.d.ts.map +1 -0
- package/dist/operators/mod.d.ts +5 -0
- package/dist/operators/mod.d.ts.map +1 -0
- package/dist/operators/mod.js +2 -0
- package/dist/operators/mod.js.map +7 -0
- package/dist/operators/or.d.ts +15 -0
- package/dist/operators/or.d.ts.map +1 -0
- package/dist/types/accounts.d.ts +11 -0
- package/dist/types/accounts.d.ts.map +1 -0
- package/dist/types/mod.d.ts +3 -0
- package/dist/types/mod.d.ts.map +1 -0
- package/dist/types/mod.js +1 -0
- package/dist/types/mod.js.map +7 -0
- package/dist/types/namespace.d.ts +10 -0
- package/dist/types/namespace.d.ts.map +1 -0
- package/dist/utils/caip.d.ts +2 -0
- package/dist/utils/caip.d.ts.map +1 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/mod.d.ts +3 -0
- package/dist/utils/mod.d.ts.map +1 -0
- package/dist/utils/mod.js +2 -0
- package/dist/utils/mod.js.map +7 -0
- package/hooks/package.json +8 -0
- package/operators/package.json +8 -0
- package/package.json +61 -0
- package/readme.md +15 -0
- package/src/actions/disconnect.ts +16 -0
- package/src/actions/mod.ts +1 -0
- package/src/builders/disconnect.ts +19 -0
- package/src/builders/mod.ts +1 -0
- package/src/hooks/changeAccountSubscriber.test.ts +172 -0
- package/src/hooks/changeAccountSubscriber.ts +239 -0
- package/src/hooks/mod.ts +1 -0
- package/src/operators/after.ts +8 -0
- package/src/operators/and.ts +39 -0
- package/src/operators/before.ts +12 -0
- package/src/operators/mod.ts +11 -0
- package/src/operators/or.ts +19 -0
- package/src/types/accounts.ts +13 -0
- package/src/types/mod.ts +11 -0
- package/src/types/namespace.ts +26 -0
- package/src/utils/caip.ts +10 -0
- package/src/utils/error.ts +44 -0
- package/src/utils/mod.ts +2 -0
- package/types/package.json +8 -0
- package/utils/package.json +8 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { AutoImplementedActionsByRecommended } from '../types/namespace.js';
|
|
2
|
+
import type { Actions } from '@hub3js/core';
|
|
3
|
+
|
|
4
|
+
import { createStore, Namespace } from '@hub3js/core';
|
|
5
|
+
import { waitFor } from '@testing-library/dom';
|
|
6
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { ChangeAccountSubscriberBuilder } from './changeAccountSubscriber.js';
|
|
9
|
+
|
|
10
|
+
interface TestNamespaceActions {
|
|
11
|
+
disconnect: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('check changeAccountSubscriber', () => {
|
|
15
|
+
const garbageProvider = { name: 'garbage provider' };
|
|
16
|
+
|
|
17
|
+
const garbageFormatter = async (
|
|
18
|
+
_instance: typeof garbageProvider,
|
|
19
|
+
accounts: string[],
|
|
20
|
+
) => accounts.map((account) => `Formatted: ${account}`);
|
|
21
|
+
|
|
22
|
+
const garbageAddEventListener = (
|
|
23
|
+
_instance: typeof garbageProvider,
|
|
24
|
+
callback: (event: string[]) => void,
|
|
25
|
+
) => {
|
|
26
|
+
callback(['0']);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const garbageRemoveEventListener = (
|
|
30
|
+
_: typeof garbageProvider,
|
|
31
|
+
__: (event: string[]) => void,
|
|
32
|
+
) => {};
|
|
33
|
+
|
|
34
|
+
let builder: ChangeAccountSubscriberBuilder<
|
|
35
|
+
string[],
|
|
36
|
+
typeof garbageProvider,
|
|
37
|
+
Actions<AutoImplementedActionsByRecommended>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
const setupNamespace = () => {
|
|
41
|
+
const actions = new Map();
|
|
42
|
+
const disconnectAction = vi.fn();
|
|
43
|
+
actions.set('disconnect', disconnectAction);
|
|
44
|
+
|
|
45
|
+
const store = createStore();
|
|
46
|
+
const ns = new Namespace<TestNamespaceActions>('evm', 'garbage provider', {
|
|
47
|
+
actions,
|
|
48
|
+
store,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const context = { action: ns.run.bind(ns), state: ns.state.bind(ns) };
|
|
52
|
+
|
|
53
|
+
return { ns, context, disconnectAction };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
builder = new ChangeAccountSubscriberBuilder();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('throws error if required operators are not set', () => {
|
|
61
|
+
expect(() => builder.build()).toThrow(
|
|
62
|
+
`Required "getInstance" operation has not been set for "changeAccountSubscriber"`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
builder.getInstance(() => garbageProvider);
|
|
66
|
+
expect(() => builder.build()).toThrow(
|
|
67
|
+
`Required "format" operation has not been set for "changeAccountSubscriber"`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
builder.format(garbageFormatter);
|
|
71
|
+
expect(() => builder.build()).toThrow(
|
|
72
|
+
`Required "addEventListener" operation has not been set for "changeAccountSubscriber"`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
builder.addEventListener(garbageAddEventListener);
|
|
76
|
+
expect(() => builder.build()).toThrow(
|
|
77
|
+
`Required "removeEventListener" operation has not been set for "changeAccountSubscriber"`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
builder.removeEventListener(garbageRemoveEventListener);
|
|
81
|
+
|
|
82
|
+
// should not throw now
|
|
83
|
+
builder.build();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('calls addEventListener and formats accounts correctly', async () => {
|
|
87
|
+
const { context } = setupNamespace();
|
|
88
|
+
const spiedAddEventListener = vi.fn(garbageAddEventListener);
|
|
89
|
+
|
|
90
|
+
builder
|
|
91
|
+
.getInstance(() => garbageProvider)
|
|
92
|
+
.format(garbageFormatter)
|
|
93
|
+
.addEventListener(spiedAddEventListener)
|
|
94
|
+
.removeEventListener(garbageRemoveEventListener);
|
|
95
|
+
|
|
96
|
+
const [subscriber] = builder.build();
|
|
97
|
+
|
|
98
|
+
expect(spiedAddEventListener).toHaveBeenCalledTimes(0);
|
|
99
|
+
subscriber(context);
|
|
100
|
+
expect(spiedAddEventListener).toHaveBeenCalledTimes(1);
|
|
101
|
+
|
|
102
|
+
const [getState] = context.state();
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(getState('accounts')?.[0]).toBe('Formatted: 0');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('wont update accounts if preventDefault called', async () => {
|
|
109
|
+
const { context } = setupNamespace();
|
|
110
|
+
const spiedAddEventListener = vi.fn(garbageAddEventListener);
|
|
111
|
+
const [getState, setState] = context.state();
|
|
112
|
+
setState('accounts', ['Initial State']);
|
|
113
|
+
builder
|
|
114
|
+
.getInstance(() => garbageProvider)
|
|
115
|
+
.format(garbageFormatter)
|
|
116
|
+
.addEventListener(spiedAddEventListener)
|
|
117
|
+
.onSwitchAccount((event) => {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
})
|
|
120
|
+
.removeEventListener(garbageRemoveEventListener);
|
|
121
|
+
|
|
122
|
+
const [subscriber] = builder.build();
|
|
123
|
+
|
|
124
|
+
subscriber(context);
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(getState('accounts')?.[0]).toBe('Initial State');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('calls removeEventListener', () => {
|
|
131
|
+
const { context } = setupNamespace();
|
|
132
|
+
const spiedRemoveEventListener = vi.fn(garbageRemoveEventListener);
|
|
133
|
+
|
|
134
|
+
builder
|
|
135
|
+
.getInstance(() => garbageProvider)
|
|
136
|
+
.format(garbageFormatter)
|
|
137
|
+
.addEventListener(garbageAddEventListener)
|
|
138
|
+
.removeEventListener(spiedRemoveEventListener);
|
|
139
|
+
|
|
140
|
+
const [, clearSubscriber] = builder.build();
|
|
141
|
+
|
|
142
|
+
expect(spiedRemoveEventListener).toHaveBeenCalledTimes(0);
|
|
143
|
+
clearSubscriber(context);
|
|
144
|
+
expect(spiedRemoveEventListener).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('calls addEventListener returned function', () => {
|
|
148
|
+
const { context } = setupNamespace();
|
|
149
|
+
const unsubscribe = vi.fn();
|
|
150
|
+
|
|
151
|
+
const addEventListenerWithReturn = (
|
|
152
|
+
_: typeof garbageProvider,
|
|
153
|
+
__: (event: string[]) => void,
|
|
154
|
+
) => {
|
|
155
|
+
return unsubscribe;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
builder
|
|
159
|
+
.getInstance(() => garbageProvider)
|
|
160
|
+
.format(garbageFormatter)
|
|
161
|
+
.addEventListener(addEventListenerWithReturn)
|
|
162
|
+
.removeEventListener(garbageRemoveEventListener);
|
|
163
|
+
|
|
164
|
+
const [subscriber, clearSubscriber] = builder.build();
|
|
165
|
+
|
|
166
|
+
subscriber(context);
|
|
167
|
+
expect(unsubscribe).toHaveBeenCalledTimes(0);
|
|
168
|
+
|
|
169
|
+
clearSubscriber(context);
|
|
170
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { AutoImplementedActionsByRecommended } from '../types/namespace.js';
|
|
2
|
+
import type {
|
|
3
|
+
Actions,
|
|
4
|
+
Context,
|
|
5
|
+
Subscriber,
|
|
6
|
+
SubscriberCleanUp,
|
|
7
|
+
} from '@hub3js/core';
|
|
8
|
+
|
|
9
|
+
type OnSwitchAccountEvent<EventType> = {
|
|
10
|
+
payload: EventType;
|
|
11
|
+
preventDefault: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class ChangeAccountSubscriberBuilder<
|
|
15
|
+
EventType,
|
|
16
|
+
ProviderAPI,
|
|
17
|
+
ActionsType extends Actions<ActionsType> &
|
|
18
|
+
Actions<AutoImplementedActionsByRecommended>,
|
|
19
|
+
> {
|
|
20
|
+
#getInstance: (() => ProviderAPI) | null = null;
|
|
21
|
+
#format:
|
|
22
|
+
| ((instance: ProviderAPI, event: EventType) => Promise<string[]>)
|
|
23
|
+
| null = null;
|
|
24
|
+
#onSwitchAccount:
|
|
25
|
+
| ((
|
|
26
|
+
event: OnSwitchAccountEvent<EventType>,
|
|
27
|
+
context: Context<ActionsType>,
|
|
28
|
+
) => void)
|
|
29
|
+
| null = null;
|
|
30
|
+
|
|
31
|
+
#addEventListener:
|
|
32
|
+
| ((
|
|
33
|
+
instance: ProviderAPI,
|
|
34
|
+
callback: (event: EventType) => void,
|
|
35
|
+
) => (() => void) | void)
|
|
36
|
+
| null = null;
|
|
37
|
+
#removeEventListener:
|
|
38
|
+
| ((instance: ProviderAPI, callback: (event: EventType) => void) => void)
|
|
39
|
+
| null = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sets the function that provides the provider API instance.
|
|
43
|
+
*
|
|
44
|
+
* @param operator - Function that returns the provider API instance
|
|
45
|
+
* @returns The builder instance for method chaining
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* builder.getInstance(() => window.ethereum)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
public getInstance(operator: () => ProviderAPI) {
|
|
52
|
+
this.#getInstance = operator;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sets the formatter function that converts provider events to account strings.
|
|
58
|
+
*
|
|
59
|
+
* @param operator - Function that takes a provider instance and event, returns array of account strings
|
|
60
|
+
* @returns The builder instance for method chaining
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* builder.format(async (instance, event) => event.accounts || [])
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
public format(
|
|
67
|
+
operator: (instance: ProviderAPI, event: EventType) => Promise<string[]>,
|
|
68
|
+
) {
|
|
69
|
+
this.#format = operator;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set a handler that runs whenever a **switch-account** event occurs.
|
|
75
|
+
*
|
|
76
|
+
* The provided operator is called with:
|
|
77
|
+
* - `event`: An object containing:
|
|
78
|
+
* - `payload`: The event payload associated with the account switch.
|
|
79
|
+
* - `preventDefault`: A function that prevents the default switch-account
|
|
80
|
+
* behavior (e.g., updating the active accounts list).
|
|
81
|
+
* - `context`: The execution context of the current flow or action.
|
|
82
|
+
*
|
|
83
|
+
* Calling `event.preventDefault()` inside the handler cancels the built-in
|
|
84
|
+
* account switching logic.
|
|
85
|
+
*
|
|
86
|
+
* @param operator - A function invoked on each switch-account event.
|
|
87
|
+
* It receives `(event, context)` and can call `event.preventDefault()` to
|
|
88
|
+
* stop the default behavior.
|
|
89
|
+
*
|
|
90
|
+
* @returns The builder instance for method chaining.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* builder.onSwitchAccount((event, context) => {
|
|
95
|
+
* if (!event.payload.userConfirmed) {
|
|
96
|
+
* event.preventDefault(); // cancel default account switch
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* console.log("Switching account:", event.payload.accountId);
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
public onSwitchAccount(
|
|
104
|
+
operator: (
|
|
105
|
+
event: OnSwitchAccountEvent<EventType>,
|
|
106
|
+
context: Context<ActionsType>,
|
|
107
|
+
) => void,
|
|
108
|
+
) {
|
|
109
|
+
this.#onSwitchAccount = operator;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sets the event listener attachment function.
|
|
115
|
+
*
|
|
116
|
+
* @param operator - Function that attaches an event listener and optionally returns a cleanup function
|
|
117
|
+
* @returns The builder instance for method chaining
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* builder.addEventListener((instance, callback) => {
|
|
121
|
+
* return instance.on('accountsChanged', callback);
|
|
122
|
+
* })
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
public addEventListener(
|
|
126
|
+
operator: (
|
|
127
|
+
instance: ProviderAPI,
|
|
128
|
+
callback: (event: EventType) => void,
|
|
129
|
+
) => (() => void) | void,
|
|
130
|
+
) {
|
|
131
|
+
this.#addEventListener = operator;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sets the event listener removal function.
|
|
137
|
+
*
|
|
138
|
+
* @param operator - Function that removes an event listener from the provider
|
|
139
|
+
* @returns The builder instance for method chaining
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* builder.removeEventListener((instance, callback) => instance.off('accountsChanged', callback))
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
public removeEventListener(
|
|
146
|
+
operator: (
|
|
147
|
+
instance: ProviderAPI,
|
|
148
|
+
callback: (event: EventType) => void,
|
|
149
|
+
) => void,
|
|
150
|
+
) {
|
|
151
|
+
this.#removeEventListener = operator;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public build(): [Subscriber<ActionsType>, SubscriberCleanUp<ActionsType>] {
|
|
156
|
+
if (this.#getInstance === null) {
|
|
157
|
+
throw new Error(this.#getErrorMessage('getInstance'));
|
|
158
|
+
}
|
|
159
|
+
if (this.#format === null) {
|
|
160
|
+
throw new Error(this.#getErrorMessage('format'));
|
|
161
|
+
}
|
|
162
|
+
if (this.#addEventListener === null) {
|
|
163
|
+
throw new Error(this.#getErrorMessage('addEventListener'));
|
|
164
|
+
}
|
|
165
|
+
if (this.#removeEventListener === null) {
|
|
166
|
+
throw new Error(this.#getErrorMessage('removeEventListener'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
* Capture current operator state at build time to ensure immutability.
|
|
171
|
+
*
|
|
172
|
+
* This creates a snapshot of all operators, preventing the built subscriber
|
|
173
|
+
* from being affected by subsequent changes to the builder instance.
|
|
174
|
+
* Each call to build() gets its own isolated set of operators, allowing
|
|
175
|
+
* the builder to be reused for creating multiple independent subscribers.
|
|
176
|
+
*/
|
|
177
|
+
const getInstance = this.#getInstance;
|
|
178
|
+
const format = this.#format;
|
|
179
|
+
const addEventListener = this.#addEventListener;
|
|
180
|
+
const removeEventListener = this.#removeEventListener;
|
|
181
|
+
const onSwitchAccount = this.#onSwitchAccount;
|
|
182
|
+
|
|
183
|
+
let subscriber: (event: EventType) => void;
|
|
184
|
+
let unsubscribe: (() => void) | void;
|
|
185
|
+
return [
|
|
186
|
+
async (context) => {
|
|
187
|
+
const [, setState] = context.state();
|
|
188
|
+
const instance = getInstance();
|
|
189
|
+
|
|
190
|
+
if (!instance) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
'Trying to subscribe to your wallet, but seems its instance is not available.',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
subscriber = async (event) => {
|
|
196
|
+
let shouldProceedWithDefault = true;
|
|
197
|
+
onSwitchAccount?.(
|
|
198
|
+
{
|
|
199
|
+
payload: event,
|
|
200
|
+
preventDefault: () => {
|
|
201
|
+
shouldProceedWithDefault = false;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
context,
|
|
205
|
+
);
|
|
206
|
+
if (!shouldProceedWithDefault) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
setState('accounts', await format(instance, event));
|
|
210
|
+
};
|
|
211
|
+
unsubscribe = addEventListener(instance, subscriber);
|
|
212
|
+
},
|
|
213
|
+
(_, err) => {
|
|
214
|
+
/*
|
|
215
|
+
* Call the cleanup function if addEventListener returned one.
|
|
216
|
+
* This handles providers that return an unsubscribe function from
|
|
217
|
+
* their event listeners.
|
|
218
|
+
*/
|
|
219
|
+
if (unsubscribe && typeof unsubscribe === 'function') {
|
|
220
|
+
unsubscribe();
|
|
221
|
+
}
|
|
222
|
+
const instance = getInstance();
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
* Always call removeEventListener as well to handle the on/off pattern.
|
|
226
|
+
* This ensures cleanup works regardless of which pattern the provider uses.
|
|
227
|
+
*/
|
|
228
|
+
removeEventListener(instance, subscriber);
|
|
229
|
+
|
|
230
|
+
// subscriber can be passed to `or`; rethrow to chain into next `or`.
|
|
231
|
+
return err;
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#getErrorMessage(operatorName: string) {
|
|
237
|
+
return `Required "${operatorName}" operation has not been set for "changeAccountSubscriber"`;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/hooks/mod.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChangeAccountSubscriberBuilder } from './changeAccountSubscriber.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Accounts, AccountsWithActiveChain } from '../types/accounts.js';
|
|
2
|
+
import type { Context } from '@hub3js/core';
|
|
3
|
+
|
|
4
|
+
import { isValidCaipAddress } from '../utils/caip.js';
|
|
5
|
+
|
|
6
|
+
export function connectAndUpdateStateForSingleNetwork(
|
|
7
|
+
context: Context,
|
|
8
|
+
accounts: Accounts,
|
|
9
|
+
) {
|
|
10
|
+
if (!accounts.every(isValidCaipAddress)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Your provider should format account addresses in CAIP-10 format. Your provided accounts: ${accounts}`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [, setState] = context.state();
|
|
17
|
+
setState('accounts', accounts);
|
|
18
|
+
setState('connected', true);
|
|
19
|
+
return accounts;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function connectAndUpdateStateForMultiNetworks(
|
|
23
|
+
context: Context,
|
|
24
|
+
accounts: AccountsWithActiveChain,
|
|
25
|
+
) {
|
|
26
|
+
if (!accounts.accounts.every(isValidCaipAddress)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Your provider should format account addresses in CAIP-10 format. Your provided accounts: ${accounts.accounts}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [, setState] = context.state();
|
|
33
|
+
setState('accounts', accounts.accounts);
|
|
34
|
+
setState('network', accounts.network);
|
|
35
|
+
setState('connected', true);
|
|
36
|
+
return accounts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const recommended = [];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Context } from '@hub3js/core';
|
|
2
|
+
|
|
3
|
+
export function intoConnecting(context: Context) {
|
|
4
|
+
const [, setState] = context.state();
|
|
5
|
+
setState('connecting', true);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recommended `before` hooks every namespace should wire up. Tuple shape
|
|
10
|
+
* matches what `NamespaceBuilder.before(...)` expects.
|
|
11
|
+
*/
|
|
12
|
+
export const recommended = [['connect', intoConnecting] as const];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { intoConnecting, recommended as beforeRecommended } from './before.js';
|
|
2
|
+
export {
|
|
3
|
+
intoConnectionFinished,
|
|
4
|
+
recommended as afterRecommended,
|
|
5
|
+
} from './after.js';
|
|
6
|
+
export {
|
|
7
|
+
connectAndUpdateStateForMultiNetworks,
|
|
8
|
+
connectAndUpdateStateForSingleNetwork,
|
|
9
|
+
recommended as andRecommended,
|
|
10
|
+
} from './and.js';
|
|
11
|
+
export { standardizeAndThrowError } from './or.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Context } from '@hub3js/core';
|
|
2
|
+
|
|
3
|
+
import { parseErrorAndThrowStandardizeError } from '../utils/error.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Standardizes an unknown error into an `Error` object and throws it. If the
|
|
7
|
+
* input is already an `Error`, it's thrown directly. Otherwise a new `Error`
|
|
8
|
+
* is created with the input's message or string representation.
|
|
9
|
+
*
|
|
10
|
+
* The actual standardization lives in `parseErrorAndThrowStandardizeError`,
|
|
11
|
+
* which can be called independently outside an action context.
|
|
12
|
+
*
|
|
13
|
+
* @param _context - The action context (unused).
|
|
14
|
+
* @param e - The unknown error object.
|
|
15
|
+
* @throws {Error} - The standardized `Error` object.
|
|
16
|
+
*/
|
|
17
|
+
export function standardizeAndThrowError(_context: Context, e: unknown): never {
|
|
18
|
+
parseErrorAndThrowStandardizeError(e);
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type CaipNamespace = string;
|
|
2
|
+
type CaipChainId = string;
|
|
3
|
+
type CaipAccountAddress = string;
|
|
4
|
+
|
|
5
|
+
export type CaipAccount =
|
|
6
|
+
`${CaipNamespace}:${CaipChainId}:${CaipAccountAddress}`;
|
|
7
|
+
|
|
8
|
+
export type Accounts = CaipAccount[];
|
|
9
|
+
|
|
10
|
+
export type AccountsWithActiveChain = {
|
|
11
|
+
accounts: Accounts;
|
|
12
|
+
network: string;
|
|
13
|
+
};
|
package/src/types/mod.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* These are the supported namespace tags. The opaque `(string & {})` widening
|
|
3
|
+
* keeps the union open — downstream packages can register arbitrary names —
|
|
4
|
+
* while preserving autocomplete for the well-known ones.
|
|
5
|
+
*/
|
|
6
|
+
type KnownNamespace =
|
|
7
|
+
| 'EVM'
|
|
8
|
+
| 'Solana'
|
|
9
|
+
| 'Cosmos'
|
|
10
|
+
| 'UTXO'
|
|
11
|
+
| 'Starknet'
|
|
12
|
+
| 'Tron'
|
|
13
|
+
| 'Ton'
|
|
14
|
+
| 'Sui'
|
|
15
|
+
| 'XRPL'
|
|
16
|
+
| 'Stellar';
|
|
17
|
+
|
|
18
|
+
export type Namespace = KnownNamespace | (string & {});
|
|
19
|
+
|
|
20
|
+
export interface CommonActions {
|
|
21
|
+
init: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AutoImplementedActionsByRecommended {
|
|
25
|
+
disconnect: () => void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardizes an unknown error into an `Error` object and throws it.
|
|
3
|
+
* If the input is already an `Error`, it's thrown directly. Otherwise a new
|
|
4
|
+
* `Error` is created with the input's message or string representation,
|
|
5
|
+
* preserving `code` and `data` fields when present.
|
|
6
|
+
*
|
|
7
|
+
* @param e - The unknown error object.
|
|
8
|
+
* @throws {Error} - The standardized `Error` object.
|
|
9
|
+
*/
|
|
10
|
+
export function parseErrorAndThrowStandardizeError(e: unknown): never {
|
|
11
|
+
if (e instanceof Error) {
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let errorMessage: string;
|
|
16
|
+
if (
|
|
17
|
+
typeof e === 'object' &&
|
|
18
|
+
e !== null &&
|
|
19
|
+
'message' in e &&
|
|
20
|
+
typeof e.message === 'string'
|
|
21
|
+
) {
|
|
22
|
+
errorMessage = e.message;
|
|
23
|
+
} else if (typeof e === 'string') {
|
|
24
|
+
errorMessage = e;
|
|
25
|
+
} else {
|
|
26
|
+
errorMessage = String(e);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const err = new Error(errorMessage) as Error & {
|
|
30
|
+
code?: unknown;
|
|
31
|
+
data?: unknown;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (typeof e === 'object' && e !== null) {
|
|
35
|
+
if ('code' in e) {
|
|
36
|
+
err.code = e.code;
|
|
37
|
+
}
|
|
38
|
+
if ('data' in e) {
|
|
39
|
+
err.data = e.data;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
package/src/utils/mod.ts
ADDED