@kheopskit/core 0.0.1-alpha.1 → 0.0.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.
@@ -1,76 +1,207 @@
1
- import type { EthereumAccount, EthereumWallet } from "@/api/types";
1
+ import type {
2
+ EthereumAccount,
3
+ EthereumAppKitWallet,
4
+ EthereumInjectedWallet,
5
+ EthereumWallet,
6
+ } from "@/api/types";
2
7
  import { getWalletAccountId } from "@/utils";
8
+ import { getCachedObservable$ } from "@/utils/getCachedObservable";
9
+ import type UniversalProvider from "@walletconnect/universal-provider";
3
10
  import {
11
+ Observable,
12
+ ReplaySubject,
4
13
  combineLatest,
14
+ distinctUntilChanged,
5
15
  map,
6
- Observable,
7
16
  of,
8
17
  shareReplay,
9
18
  switchMap,
10
19
  } from "rxjs";
11
- import { getAddress, type EIP1193Provider } from "viem";
12
- import { ethereumWallets$ } from "./wallets";
20
+ import {
21
+ type EIP1193Provider,
22
+ createWalletClient,
23
+ custom,
24
+ getAddress,
25
+ } from "viem";
13
26
 
14
- const getWalletAccounts$ = (
15
- wallet: EthereumWallet
27
+ const getInjectedWalletAccounts$ = (
28
+ wallet: EthereumInjectedWallet,
16
29
  ): Observable<EthereumAccount[]> => {
17
30
  if (!wallet.isConnected) return of([]);
18
31
 
19
- return new Observable<EthereumAccount[]>((subscriber) => {
20
- const getAccount = (address: string, i: number): EthereumAccount => ({
21
- id: getWalletAccountId(wallet.id, address),
22
- platform: "ethereum",
23
- provider: wallet.provider as EIP1193Provider,
24
- address: getAddress(address),
25
- walletName: wallet.name,
26
- walletId: wallet.id,
27
- isWalletDefault: i === 0,
28
- });
29
-
30
- const listener = (addresses: string[]) => {
31
- subscriber.next(addresses.map(getAccount));
32
- };
32
+ return getCachedObservable$(`accounts:${wallet.id}`, () =>
33
+ new Observable<EthereumAccount[]>((subscriber) => {
34
+ const getAccount = (address: string, i: number): EthereumAccount => {
35
+ // console.log("[Injected] createWalletClient", address);
36
+ const client = createWalletClient({
37
+ account: address as `0x${string}`,
38
+ transport: custom(wallet.provider as EIP1193Provider),
39
+ });
33
40
 
34
- // subscribe to changes
35
- wallet.provider.on("accountsChanged", listener);
41
+ return {
42
+ id: getWalletAccountId(wallet.id, address),
43
+ platform: "ethereum",
44
+ client,
45
+ address: getAddress(address),
46
+ walletName: wallet.name,
47
+ walletId: wallet.id,
48
+ isWalletDefault: i === 0,
49
+ };
50
+ };
36
51
 
37
- // initial value
38
- wallet.provider
39
- .request({ method: "eth_accounts" })
40
- .then((addresses: string[]) => {
52
+ const handleAccountsChanged = (addresses: string[]) => {
41
53
  subscriber.next(addresses.map(getAccount));
42
- })
43
- .catch((err) => {
44
- console.error("Failed to get accounts", err);
45
- subscriber.next([]);
46
- });
54
+ };
47
55
 
48
- return () => {
49
- wallet.provider.removeListener("accountsChanged", listener);
50
- };
56
+ // subscribe to changes
57
+ wallet.provider.on("accountsChanged", handleAccountsChanged);
58
+
59
+ // initial value
60
+ wallet.provider
61
+ .request({ method: "eth_accounts" })
62
+ .then((addresses: string[]) => {
63
+ subscriber.next(addresses.map(getAccount));
64
+ })
65
+ .catch((err) => {
66
+ console.error("Failed to get accounts", err);
67
+ subscriber.next([]);
68
+ });
69
+
70
+ return () => {
71
+ wallet.provider.removeListener(
72
+ "accountsChanged",
73
+ handleAccountsChanged,
74
+ );
75
+ };
76
+ }).pipe(shareReplay({ refCount: true, bufferSize: 1 })),
77
+ );
78
+ };
79
+
80
+ const wrapWalletConnectProvider = (
81
+ provider: EIP1193Provider,
82
+ sessionTopic: string,
83
+ caipNetworkId: string,
84
+ ): EIP1193Provider => {
85
+ return new Proxy(provider, {
86
+ get(target, prop, receiver) {
87
+ if (prop !== "request") return Reflect.get(target, prop, receiver);
88
+
89
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
90
+ return (args: any) => {
91
+ // console.log("request", { target, prop, receiver, caipNetworkId, args });
92
+
93
+ if (args && typeof args === "object" && args.method) {
94
+ if (!args.topic) args.topic = sessionTopic;
95
+ if (!args.chainId) args.chainId = caipNetworkId;
96
+ }
97
+ return target.request(args);
98
+ };
99
+ },
51
100
  });
52
101
  };
53
102
 
54
- export const ethereumAccounts$ = new Observable<EthereumAccount[]>(
55
- (subscriber) => {
56
- const sub = ethereumWallets$
103
+ const getAppKitAccounts$ = (
104
+ wallet: EthereumAppKitWallet,
105
+ ): Observable<EthereumAccount[]> => {
106
+ const account = wallet.appKit.getAccount("eip155");
107
+ const provider = wallet.appKit.getProvider<UniversalProvider>("eip155");
108
+
109
+ if (
110
+ !wallet.isConnected ||
111
+ !wallet.appKit ||
112
+ !account?.allAccounts.length ||
113
+ !provider?.session
114
+ )
115
+ return of([]);
116
+
117
+ return getCachedObservable$("accounts:appKit", () =>
118
+ new Observable<EthereumAccount[]>((subscriber) => {
119
+ const caipNetworkId$ = new ReplaySubject<string>(1);
120
+
121
+ const handleChainChanged = (chainId: unknown) => {
122
+ caipNetworkId$.next(`eip155:${chainId}`);
123
+ };
124
+
125
+ provider.on("chainChanged", handleChainChanged);
126
+ provider.request({ method: "eth_chainId" }).then(handleChainChanged);
127
+
128
+ const sub = caipNetworkId$
129
+ .pipe(
130
+ distinctUntilChanged(),
131
+ map((caipNetworkId) =>
132
+ custom(
133
+ wrapWalletConnectProvider(
134
+ provider as EIP1193Provider,
135
+ // biome-ignore lint/style/noNonNullAssertion: <explanation>
136
+ provider.session!.topic,
137
+ caipNetworkId,
138
+ ),
139
+ ),
140
+ ),
141
+ map((transport) =>
142
+ account.allAccounts.map((acc, i): EthereumAccount => {
143
+ // console.log("[AppKit] createWalletClient", acc);
144
+ const client = createWalletClient({
145
+ account: acc.address as `0x${string}`,
146
+ transport,
147
+ });
148
+
149
+ return {
150
+ id: getWalletAccountId(wallet.id, acc.address),
151
+ platform: "ethereum",
152
+ walletName: wallet.name,
153
+ walletId: wallet.id,
154
+ address: acc.address as `0x${string}`,
155
+ client,
156
+ isWalletDefault: i === 0,
157
+ };
158
+ }),
159
+ ),
160
+ )
161
+ .subscribe(subscriber);
162
+
163
+ return () => {
164
+ provider.off("chainChanged", handleChainChanged);
165
+ sub.unsubscribe();
166
+ };
167
+ }).pipe(shareReplay({ refCount: true, bufferSize: 1 })),
168
+ );
169
+ };
170
+
171
+ export const getEthereumAccounts$ = (
172
+ ethereumWallets: Observable<EthereumWallet[]>,
173
+ ) =>
174
+ new Observable<EthereumAccount[]>((subscriber) => {
175
+ const sub = ethereumWallets
57
176
  .pipe(
58
177
  map((wallets) => wallets.filter((w) => w.isConnected)),
59
- switchMap((wallets) =>
60
- wallets.length
61
- ? combineLatest(wallets.map(getWalletAccounts$))
62
- : of([])
63
- ),
64
- map((accounts) => accounts.flat())
178
+ switchMap((wallets) => {
179
+ return wallets.length
180
+ ? combineLatest([
181
+ ...wallets
182
+ .filter((w) => w.type === "injected")
183
+ .map(getInjectedWalletAccounts$),
184
+ ...wallets
185
+ .filter((w) => w.type === "appKit")
186
+ .map(getAppKitAccounts$),
187
+ // todo appkit
188
+ ])
189
+ : of([]);
190
+ }),
191
+ map((accounts) => accounts.flat()),
192
+ distinctUntilChanged(isSameAccountsList),
65
193
  )
66
194
  .subscribe(subscriber);
67
195
 
68
196
  return () => {
69
197
  sub.unsubscribe();
70
198
  };
71
- }
72
- ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
199
+ }).pipe(
200
+ // logObservable("ethereumAccounts$", true),
201
+ shareReplay({ refCount: true, bufferSize: 1 }),
202
+ );
73
203
 
74
- ethereumAccounts$.subscribe(() => {
75
- console.count("[kheopskit] ethereumAccounts$ emit");
76
- });
204
+ const isSameAccountsList = (a: EthereumAccount[], b: EthereumAccount[]) => {
205
+ if (a.length !== b.length) return false;
206
+ return a.every((account, i) => account.id === b[i]?.id);
207
+ };
@@ -1,15 +1,20 @@
1
1
  import { store } from "@/api/store";
2
- import type { EthereumWallet } from "@/api/types";
3
- import { getWalletId, type WalletId } from "@/utils/WalletId";
4
- import { createStore, type EIP6963ProviderDetail } from "mipd";
2
+ import type {
3
+ EthereumInjectedWallet,
4
+ EthereumWallet,
5
+ KheopskitConfig,
6
+ } from "@/api/types";
7
+ import { type WalletId, getWalletId } from "@/utils/WalletId";
8
+ import { type EIP6963ProviderDetail, createStore } from "mipd";
5
9
  import {
6
10
  BehaviorSubject,
11
+ Observable,
7
12
  combineLatest,
8
13
  map,
9
- Observable,
10
14
  shareReplay,
11
15
  } from "rxjs";
12
16
  import type { EIP1193Provider } from "viem";
17
+ import { getAppKitWallets$ } from "../appKit";
13
18
 
14
19
  const providersDetails$ = new Observable<EIP6963ProviderDetail[]>(
15
20
  (subscriber) => {
@@ -27,25 +32,21 @@ const providersDetails$ = new Observable<EIP6963ProviderDetail[]>(
27
32
  unsubscribe();
28
33
  store.destroy();
29
34
  };
30
- }
35
+ },
31
36
  ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
32
37
 
33
- providersDetails$.subscribe(() => {
34
- console.count("[kheopskit] providers$ emit");
35
- });
36
-
37
- export const ethereumWallets$ = new Observable<EthereumWallet[]>(
38
+ const ethereumInjectedWallets$ = new Observable<EthereumInjectedWallet[]>(
38
39
  (subscriber) => {
39
40
  const enabledWalletIds$ = new BehaviorSubject<Set<WalletId>>(new Set());
40
41
 
41
42
  const connectWallet = async (
42
43
  walletId: WalletId,
43
- provider: EIP1193Provider
44
+ provider: EIP1193Provider,
44
45
  ) => {
45
46
  if (enabledWalletIds$.value.has(walletId))
46
47
  throw new Error(`Extension ${walletId} already connected`);
47
48
 
48
- provider.request({
49
+ await provider.request({
49
50
  method: "eth_requestAccounts",
50
51
  });
51
52
 
@@ -56,10 +57,7 @@ export const ethereumWallets$ = new Observable<EthereumWallet[]>(
56
57
  store.addEnabledWalletId(walletId);
57
58
  };
58
59
 
59
- const disconnectWallet = async (
60
- walletId: WalletId,
61
- _provider: EIP1193Provider
62
- ) => {
60
+ const disconnectWallet = async (walletId: WalletId) => {
63
61
  if (!enabledWalletIds$.value.has(walletId))
64
62
  throw new Error(`Extension ${walletId} is not connected`);
65
63
  const newSet = new Set(enabledWalletIds$.value);
@@ -72,12 +70,13 @@ export const ethereumWallets$ = new Observable<EthereumWallet[]>(
72
70
  const sub = combineLatest([providersDetails$, enabledWalletIds$])
73
71
  .pipe(
74
72
  map(([providerDetails, enabledWalletIds]) => {
75
- return providerDetails.map((pd): EthereumWallet => {
73
+ return providerDetails.map((pd): EthereumInjectedWallet => {
76
74
  const walletId = getWalletId("ethereum", pd.info.rdns);
77
75
  const provider = pd.provider as EIP1193Provider;
78
76
 
79
77
  return {
80
78
  platform: "ethereum",
79
+ type: "injected",
81
80
  id: walletId,
82
81
  name: pd.info.name,
83
82
  icon: pd.info.icon,
@@ -85,19 +84,34 @@ export const ethereumWallets$ = new Observable<EthereumWallet[]>(
85
84
  isConnected: enabledWalletIds.has(walletId),
86
85
  providerId: pd.info.rdns,
87
86
  connect: () => connectWallet(walletId, provider),
88
- disconnect: () => disconnectWallet(walletId, provider),
87
+ disconnect: () => disconnectWallet(walletId),
89
88
  };
90
89
  });
91
- })
90
+ }),
92
91
  )
93
92
  .subscribe(subscriber);
94
93
 
95
94
  return () => {
96
95
  sub.unsubscribe();
97
96
  };
98
- }
97
+ },
99
98
  ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
100
99
 
101
- ethereumWallets$.subscribe(() => {
102
- console.count("[kheopskit] ethereumWallets$ emit");
103
- });
100
+ export const getEthereumWallets$ = (config: KheopskitConfig) => {
101
+ return new Observable<EthereumWallet[]>((subscriber) => {
102
+ const subscription = combineLatest([
103
+ ethereumInjectedWallets$,
104
+ getAppKitWallets$(config)?.pipe(map((w) => w.ethereum)),
105
+ ])
106
+ .pipe(
107
+ map(([injectedWallets, appKitWallet]) =>
108
+ appKitWallet ? [...injectedWallets, appKitWallet] : injectedWallets,
109
+ ),
110
+ )
111
+ .subscribe(subscriber);
112
+
113
+ return () => {
114
+ subscription.unsubscribe();
115
+ };
116
+ }).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
117
+ };
package/src/api/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./kheopskit";
2
2
  export * from "./types";
3
+ export { resolveConfig } from "./config";
@@ -1,4 +1,11 @@
1
- import { combineLatest, map, Observable, shareReplay } from "rxjs";
1
+ import { logObservable } from "@/utils/logObservable";
2
+ import {
3
+ Observable,
4
+ combineLatest,
5
+ map,
6
+ shareReplay,
7
+ throttleTime,
8
+ } from "rxjs";
2
9
  import { getAccounts$ } from "./accounts";
3
10
  import { resolveConfig } from "./config";
4
11
  import type { KheopskitConfig, Wallet, WalletAccount } from "./types";
@@ -6,18 +13,33 @@ import { getWallets$ } from "./wallets";
6
13
 
7
14
  export type { KheopskitConfig } from "./types";
8
15
 
9
- export const getKheopskit$ = (config: KheopskitConfig) => {
10
- const c = resolveConfig(config);
16
+ export type KheopskitState = {
17
+ wallets: Wallet[];
18
+ accounts: WalletAccount[];
19
+ config: KheopskitConfig;
20
+ };
21
+
22
+ export const getKheopskit$ = (config?: Partial<KheopskitConfig>) => {
23
+ const kc = resolveConfig(config);
24
+
25
+ console.debug("[kheopskit] config", kc);
26
+
27
+ return new Observable<KheopskitState>((subscriber) => {
28
+ const wallets$ = getWallets$(kc);
11
29
 
12
- return new Observable<{ wallets: Wallet[]; accounts: WalletAccount[] }>(
13
- (subscriber) => {
14
- const subscription = combineLatest([getWallets$(c), getAccounts$(c)])
15
- .pipe(map(([wallets, accounts]) => ({ wallets, accounts })))
16
- .subscribe(subscriber);
30
+ const subscription = combineLatest({
31
+ wallets: wallets$,
32
+ accounts: getAccounts$(kc, wallets$),
33
+ })
34
+ .pipe(map(({ wallets, accounts }) => ({ config: kc, wallets, accounts })))
35
+ .subscribe(subscriber);
17
36
 
18
- return () => {
19
- subscription.unsubscribe();
20
- };
21
- }
22
- ).pipe(shareReplay({ bufferSize: 1, refCount: true }));
37
+ return () => {
38
+ subscription.unsubscribe();
39
+ };
40
+ }).pipe(
41
+ throttleTime(50, undefined, { leading: true, trailing: true }),
42
+ logObservable("kheopskit$", { enabled: kc.debug, printValue: true }),
43
+ shareReplay({ bufferSize: 1, refCount: true }),
44
+ );
23
45
  };
@@ -1,18 +1,29 @@
1
- import type { PolkadotAccount, PolkadotWallet } from "@/api/types";
1
+ import type {
2
+ PolkadotAccount,
3
+ PolkadotAppKitWallet,
4
+ PolkadotInjectedWallet,
5
+ PolkadotWallet,
6
+ } from "@/api/types";
2
7
  import { getWalletAccountId } from "@/utils";
3
- import type { InjectedPolkadotAccount } from "polkadot-api/pjs-signer";
8
+ import type { AppKit } from "@reown/appkit/core";
9
+ import type UniversalProvider from "@walletconnect/universal-provider";
4
10
  import {
11
+ type InjectedExtension,
12
+ type InjectedPolkadotAccount,
13
+ getPolkadotSignerFromPjs,
14
+ } from "polkadot-api/pjs-signer";
15
+ import {
16
+ Observable,
5
17
  combineLatest,
18
+ distinctUntilChanged,
6
19
  map,
7
- Observable,
8
20
  of,
9
21
  shareReplay,
10
22
  switchMap,
11
23
  } from "rxjs";
12
- import { polkadotWallets$ } from "./wallets";
13
24
 
14
- const getWalletAccounts$ = (
15
- wallet: PolkadotWallet
25
+ const getInjectedWalletAccounts$ = (
26
+ wallet: PolkadotInjectedWallet,
16
27
  ): Observable<PolkadotAccount[]> => {
17
28
  if (!wallet.isConnected) return of([]);
18
29
 
@@ -25,13 +36,15 @@ const getWalletAccounts$ = (
25
36
  walletId: wallet.id,
26
37
  });
27
38
 
39
+ const extension = wallet.extension as InjectedExtension;
40
+
28
41
  // subscribe to changes
29
- const unsubscribe = wallet.extension.subscribe((accounts) => {
42
+ const unsubscribe = extension.subscribe((accounts) => {
30
43
  subscriber.next(accounts.map(getAccount));
31
44
  });
32
45
 
33
46
  // initial value
34
- subscriber.next(wallet.extension.getAccounts().map(getAccount));
47
+ subscriber.next(extension.getAccounts().map(getAccount));
35
48
 
36
49
  return () => {
37
50
  return unsubscribe();
@@ -39,26 +52,108 @@ const getWalletAccounts$ = (
39
52
  });
40
53
  };
41
54
 
42
- export const polkadotAccounts$ = new Observable<PolkadotAccount[]>(
43
- (subscriber) => {
55
+ const getAppKitPolkadotSigner = (appKit: AppKit, address: string) => {
56
+ const provider = appKit.getProvider<UniversalProvider>("polkadot");
57
+ if (!provider) throw new Error("No provider found");
58
+ if (!provider.session) throw new Error("No session found");
59
+
60
+ return getPolkadotSignerFromPjs(
61
+ address,
62
+ (transactionPayload) => {
63
+ if (!provider.session) throw new Error("No session found");
64
+
65
+ return provider.client.request({
66
+ topic: provider.session.topic,
67
+ chainId: `polkadot:${transactionPayload.genesisHash.substring(2, 34)}`,
68
+ request: {
69
+ method: "polkadot_signTransaction",
70
+ params: {
71
+ address,
72
+ transactionPayload,
73
+ },
74
+ },
75
+ });
76
+ },
77
+ async ({ address, data }) => {
78
+ if (!provider.session) throw new Error("No session found");
79
+ const networks = appKit.getCaipNetworks("polkadot");
80
+ const chainId = networks[0]?.caipNetworkId;
81
+ if (!chainId) throw new Error("No chainId found");
82
+
83
+ return provider.client.request({
84
+ topic: provider.session.topic,
85
+ chainId,
86
+ request: {
87
+ method: "polkadot_signMessage",
88
+ params: {
89
+ address,
90
+ message: data,
91
+ },
92
+ },
93
+ });
94
+ },
95
+ );
96
+ };
97
+
98
+ const getAppKitAccounts$ = (wallet: PolkadotAppKitWallet) => {
99
+ const account = wallet.appKit.getAccount("polkadot");
100
+ const provider = wallet.appKit.getProvider<UniversalProvider>("polkadot");
101
+
102
+ if (
103
+ !wallet.isConnected ||
104
+ !wallet.appKit ||
105
+ !account?.allAccounts.length ||
106
+ !provider?.session
107
+ )
108
+ return of([]);
109
+
110
+ return of(
111
+ account.allAccounts.map(
112
+ (acc): PolkadotAccount => ({
113
+ id: getWalletAccountId(wallet.id, acc.address),
114
+ platform: "polkadot",
115
+ walletName: wallet.name,
116
+ walletId: wallet.id,
117
+ address: acc.address,
118
+ polkadotSigner: getAppKitPolkadotSigner(wallet.appKit, acc.address),
119
+ genesisHash: null,
120
+ name: `${wallet.name} Polkadot`,
121
+ type: "sr25519",
122
+ }),
123
+ ),
124
+ );
125
+ };
126
+
127
+ export const getPolkadotAccounts$ = (
128
+ polkadotWallets$: Observable<PolkadotWallet[]>,
129
+ ) =>
130
+ new Observable<PolkadotAccount[]>((subscriber) => {
44
131
  const sub = polkadotWallets$
45
132
  .pipe(
46
133
  map((wallets) => wallets.filter((w) => w.isConnected)),
47
134
  switchMap((wallets) =>
48
135
  wallets.length
49
- ? combineLatest(wallets.map(getWalletAccounts$))
50
- : of([])
136
+ ? combineLatest([
137
+ ...wallets
138
+ .filter((w) => w.type === "injected")
139
+ .map(getInjectedWalletAccounts$),
140
+ ...wallets
141
+ .filter((w) => w.type === "appKit")
142
+ .map(getAppKitAccounts$),
143
+ ])
144
+ : of([]),
51
145
  ),
52
- map((accounts) => accounts.flat())
146
+ map((accounts) => accounts.flat()),
147
+ distinctUntilChanged(isSameAccountsList),
53
148
  )
54
149
  .subscribe(subscriber);
55
150
 
56
151
  return () => {
57
152
  sub.unsubscribe();
58
153
  };
59
- }
60
- ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
154
+ }).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
61
155
 
62
- polkadotAccounts$.subscribe(() => {
63
- console.count("[kheopskit] polkadotAccounts$ emit");
64
- });
156
+ const isSameAccountsList = (a: PolkadotAccount[], b: PolkadotAccount[]) => {
157
+ if (a.length !== b.length) return false;
158
+ return a.every((account, i) => account.id === b[i]?.id);
159
+ };