cosmos-connect-core 0.1.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 (54) hide show
  1. package/dist/adapters/CosmostationWallet.d.ts +40 -0
  2. package/dist/adapters/CosmostationWallet.js +68 -0
  3. package/dist/adapters/GalaxyStationWallet.d.ts +34 -0
  4. package/dist/adapters/GalaxyStationWallet.js +64 -0
  5. package/dist/adapters/KeplrWallet.d.ts +36 -0
  6. package/dist/adapters/KeplrWallet.js +85 -0
  7. package/dist/adapters/LUNCDashWallet.d.ts +6 -0
  8. package/dist/adapters/LUNCDashWallet.js +19 -0
  9. package/dist/adapters/LeapWallet.d.ts +33 -0
  10. package/dist/adapters/LeapWallet.js +64 -0
  11. package/dist/adapters/StationWallet.d.ts +34 -0
  12. package/dist/adapters/StationWallet.js +67 -0
  13. package/dist/adapters/WalletConnectWallet.d.ts +27 -0
  14. package/dist/adapters/WalletConnectWallet.js +89 -0
  15. package/dist/adapters/utils/QRCodeModal.d.ts +20 -0
  16. package/dist/adapters/utils/QRCodeModal.js +50 -0
  17. package/dist/adapters/utils/WalletConnectV2.d.ts +52 -0
  18. package/dist/adapters/utils/WalletConnectV2.js +284 -0
  19. package/dist/adapters/utils/os.d.ts +1 -0
  20. package/dist/adapters/utils/os.js +1 -0
  21. package/dist/core/account.d.ts +1 -0
  22. package/dist/core/account.js +7 -0
  23. package/dist/core/chain.d.ts +4 -0
  24. package/dist/core/chain.js +11 -0
  25. package/dist/core/createClient.d.ts +2 -0
  26. package/dist/core/createClient.js +157 -0
  27. package/dist/core/storage.d.ts +2 -0
  28. package/dist/core/storage.js +18 -0
  29. package/dist/core/types.d.ts +55 -0
  30. package/dist/core/types.js +1 -0
  31. package/dist/core/wallet.d.ts +2 -0
  32. package/dist/core/wallet.js +3 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +11 -0
  35. package/package.json +18 -0
  36. package/src/adapters/CosmostationWallet.ts +113 -0
  37. package/src/adapters/GalaxyStationWallet.ts +100 -0
  38. package/src/adapters/KeplrWallet.ts +126 -0
  39. package/src/adapters/LUNCDashWallet.ts +20 -0
  40. package/src/adapters/LeapWallet.ts +95 -0
  41. package/src/adapters/StationWallet.ts +100 -0
  42. package/src/adapters/WalletConnectWallet.ts +125 -0
  43. package/src/adapters/utils/QRCodeModal.ts +73 -0
  44. package/src/adapters/utils/WalletConnectV2.ts +388 -0
  45. package/src/adapters/utils/os.ts +1 -0
  46. package/src/core/account.ts +7 -0
  47. package/src/core/chain.ts +15 -0
  48. package/src/core/createClient.ts +194 -0
  49. package/src/core/storage.ts +20 -0
  50. package/src/core/types.ts +72 -0
  51. package/src/core/wallet.ts +5 -0
  52. package/src/index.ts +11 -0
  53. package/src/types/modules.d.ts +6 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,20 @@
1
+ export type MobileAppDetails = {
2
+ name: string;
3
+ android: string;
4
+ ios: string;
5
+ isStation?: boolean;
6
+ isLuncDash?: boolean;
7
+ description?: string;
8
+ url?: string;
9
+ icons?: string[];
10
+ projectId?: string;
11
+ };
12
+ export declare class QRCodeModal {
13
+ private details;
14
+ constructor(details: MobileAppDetails);
15
+ open(uri: string): void;
16
+ close(): void;
17
+ private getSchemeUri;
18
+ private generateAndroidIntent;
19
+ private generateIosIntent;
20
+ }
@@ -0,0 +1,50 @@
1
+ import { isAndroid, isMobile } from "./os.js";
2
+ export class QRCodeModal {
3
+ details;
4
+ // Expose URI for external access (monkey-patch hook)
5
+ // Static or instance property? WalletConnectV2 creates a NEW instance each time.
6
+ // So we need a way to extract it.
7
+ // The official QRCodeModal renders to DOM.
8
+ // The WalletConnectWallet adapter will PATCH the prototype or instance.
9
+ constructor(details) {
10
+ this.details = details;
11
+ }
12
+ open(uri) {
13
+ console.log("QRCodeModalStub open called with:", uri);
14
+ // Default behavior is to redirect on mobile
15
+ if (isMobile() && typeof window !== "undefined") {
16
+ const schemeUri = this.getSchemeUri(uri);
17
+ if (this.details.isStation) {
18
+ window.location.href = schemeUri;
19
+ }
20
+ else if (isAndroid()) {
21
+ window.location.href = this.generateAndroidIntent(uri);
22
+ }
23
+ else {
24
+ window.location.href = this.generateIosIntent(uri);
25
+ }
26
+ }
27
+ }
28
+ close() {
29
+ console.log("QRCodeModalStub close called");
30
+ }
31
+ getSchemeUri(uri) {
32
+ return this.details.isStation
33
+ ? this.details.isLuncDash
34
+ ? `luncdash://wallet_connect?${encodeURIComponent(`payload=${encodeURIComponent(uri)}`)}`
35
+ : `https://terrastation.page.link/?link=https://terra.money?${encodeURIComponent(`action=wallet_connect&payload=${encodeURIComponent(uri)}`)}&apn=money.terra.station&ibi=money.terra.station&isi=1548434735`
36
+ : uri;
37
+ }
38
+ generateAndroidIntent(uri) {
39
+ const hashIndex = this.details.android.indexOf("#");
40
+ if (hashIndex === -1)
41
+ return this.details.android;
42
+ return (this.details.android.slice(0, hashIndex) +
43
+ "?" +
44
+ encodeURIComponent(uri) +
45
+ this.details.android.slice(hashIndex));
46
+ }
47
+ generateIosIntent(uri) {
48
+ return this.details.ios + "?" + encodeURIComponent(uri);
49
+ }
50
+ }
@@ -0,0 +1,52 @@
1
+ import { MobileAppDetails } from "./QRCodeModal.js";
2
+ export type GetAccountResponse = {
3
+ name?: string | undefined;
4
+ address: string;
5
+ algo: string;
6
+ pubkey: string;
7
+ };
8
+ export type WcSignAminoResponse = {
9
+ signature: {
10
+ signature: string;
11
+ };
12
+ signed?: any | undefined;
13
+ };
14
+ export type SignAminoResponse = Required<WcSignAminoResponse>;
15
+ export type WcSignDirectResponse = {
16
+ signature: {
17
+ signature: string;
18
+ };
19
+ signed?: any | undefined;
20
+ };
21
+ export type SignDirectResponse = Required<WcSignDirectResponse>;
22
+ export type WalletConnectV2Config = {
23
+ disableConnectionCheck?: boolean;
24
+ };
25
+ export declare class WalletConnectV2 {
26
+ private readonly projectId;
27
+ private readonly mobileAppDetails;
28
+ private readonly sessionStorageKey;
29
+ private readonly accountStorageKey;
30
+ private readonly onDisconnectCbs;
31
+ private readonly onAccountChangeCbs;
32
+ private readonly onUriCbs;
33
+ private signClient;
34
+ private config?;
35
+ constructor(projectId: string, mobileAppDetails: MobileAppDetails, config?: WalletConnectV2Config);
36
+ onUri(cb: (uri: string) => unknown): () => void;
37
+ addChain(chainId: string, chainInfo: any): Promise<void>;
38
+ connect(chainIds: string[]): Promise<void>;
39
+ disconnect(): void;
40
+ onDisconnect(cb: () => unknown): () => void;
41
+ onAccountChange(cb: () => unknown): () => void;
42
+ getAccount(chainId: string): Promise<GetAccountResponse>;
43
+ signArbitrary(chainId: string, signerAddress: string, data: string): Promise<{
44
+ signature: string;
45
+ }>;
46
+ signAmino(chainId: string, signerAddress: string, stdSignDoc: any): Promise<SignAminoResponse>;
47
+ signDirect(chainId: string, signerAddress: string, signDoc: any): Promise<SignDirectResponse>;
48
+ private isConnected;
49
+ private _disconnect;
50
+ private request;
51
+ private toCosmosNamespace;
52
+ }
@@ -0,0 +1,284 @@
1
+ // @ts-nocheck
2
+ import SignClient from "@walletconnect/sign-client";
3
+ import { isAndroid, isMobile } from "./os.js";
4
+ import { debounce } from "lodash-es";
5
+ const Method = {
6
+ GET_ACCOUNTS: "cosmos_getAccounts",
7
+ SIGN_AMINO: "cosmos_signAmino",
8
+ SIGN_DIRECT: "cosmos_signDirect",
9
+ SIGN_ARBITRARY: "keplr_signArbitrary",
10
+ ADD_CHAIN: "keplr_experimentalSuggestChain",
11
+ };
12
+ const Event = {
13
+ CHAIN_CHANGED: "chainChanged",
14
+ ACCOUNTS_CHANGED: "accountsChanged",
15
+ };
16
+ const DEFAULT_SIGN_OPTIONS = {
17
+ preferNoSetFee: true,
18
+ preferNoSetMemo: true,
19
+ };
20
+ export class WalletConnectV2 {
21
+ projectId;
22
+ mobileAppDetails;
23
+ sessionStorageKey;
24
+ accountStorageKey;
25
+ onDisconnectCbs;
26
+ onAccountChangeCbs;
27
+ onUriCbs;
28
+ signClient;
29
+ config;
30
+ constructor(projectId, mobileAppDetails, config) {
31
+ this.projectId = projectId;
32
+ this.mobileAppDetails = mobileAppDetails;
33
+ this.sessionStorageKey = `cosmes.wallet.${mobileAppDetails.name.toLowerCase()}.wcSession`;
34
+ this.accountStorageKey = `cosmes.wallet.${mobileAppDetails.name.toLowerCase()}.lastAccount`;
35
+ this.onDisconnectCbs = new Set();
36
+ this.onAccountChangeCbs = new Set();
37
+ this.onUriCbs = new Set();
38
+ this.signClient = null;
39
+ this.config = config;
40
+ }
41
+ onUri(cb) {
42
+ this.onUriCbs.add(cb);
43
+ return () => {
44
+ this.onUriCbs.delete(cb);
45
+ };
46
+ }
47
+ async addChain(chainId, chainInfo) {
48
+ if (!this.signClient) {
49
+ throw new Error("SignClient is not initialized");
50
+ }
51
+ await this.request(chainId, Method.ADD_CHAIN, {
52
+ chainInfo,
53
+ });
54
+ }
55
+ async connect(chainIds) {
56
+ // Initialise the sign client and event listeners if they don't already exist
57
+ if (!this.signClient) {
58
+ console.log("WalletConnectV2: Initializing SignClient...");
59
+ try {
60
+ this.signClient = await SignClient.init({
61
+ projectId: this.projectId,
62
+ relayUrl: "wss://relay.walletconnect.com",
63
+ metadata: {
64
+ name: this.mobileAppDetails.name,
65
+ description: this.mobileAppDetails.description || "Cosmos App",
66
+ url: this.mobileAppDetails.url ||
67
+ (typeof window !== "undefined" ? window.location.origin : ""),
68
+ icons: this.mobileAppDetails.icons || [],
69
+ },
70
+ });
71
+ console.log("WalletConnectV2: SignClient initialized");
72
+ }
73
+ catch (err) {
74
+ console.error("WalletConnectV2: Failed to initialize SignClient", err);
75
+ throw err;
76
+ }
77
+ // Disconnect if the session is disconnected or expired
78
+ this.signClient.on("session_delete", ({ topic }) => this._disconnect(topic));
79
+ this.signClient.on("session_expire", ({ topic }) => this._disconnect(topic));
80
+ // Handle the `accountsChanged` event
81
+ const handleAccountChange = debounce(
82
+ // Handler is debounced as the `accountsChanged` event is fired once for
83
+ // each connected chain, but we only want to trigger the callback once.
84
+ () => this.onAccountChangeCbs.forEach((cb) => cb()), 300, { leading: true, trailing: false });
85
+ this.signClient.on("session_event", ({ params }) => {
86
+ if (params.event.name === Event.ACCOUNTS_CHANGED) {
87
+ handleAccountChange();
88
+ }
89
+ });
90
+ }
91
+ // Check if a valid session already exists
92
+ const oldSession = localStorage.getItem(this.sessionStorageKey);
93
+ const chainIdsSet = new Set(chainIds);
94
+ if (oldSession) {
95
+ const { topic, chainIds: storedIds } = JSON.parse(oldSession);
96
+ const storedIdsSet = new Set(storedIds);
97
+ if (chainIds.every((id) => storedIdsSet.has(id))) {
98
+ // Assume we want a fresh session for the UI to show the QR code
99
+ // unless explicitly disabled.
100
+ if (this.config?.disableConnectionCheck) {
101
+ return;
102
+ }
103
+ // Force disconnect old session to ensure a new URI is generated
104
+ this._disconnect(topic);
105
+ }
106
+ else {
107
+ // Otherwise, we need to merge the stored IDs with the requested IDs
108
+ for (const id of storedIds) {
109
+ chainIdsSet.add(id);
110
+ }
111
+ }
112
+ }
113
+ // Initialise a new session
114
+ const { uri, approval } = await this.signClient.connect({
115
+ optionalNamespaces: {
116
+ cosmos: {
117
+ chains: [...chainIdsSet].map((id) => this.toCosmosNamespace(id)),
118
+ methods: Object.values(Method),
119
+ events: Object.values(Event),
120
+ },
121
+ },
122
+ });
123
+ if (uri) {
124
+ console.log("WalletConnectV2: URI generated", uri);
125
+ this._uri = uri; // Store it locally too
126
+ this.onUriCbs.forEach((cb) => cb(uri));
127
+ console.log("WalletConnectV2: Waiting for approval...");
128
+ const approvalPromise = approval();
129
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Connection approval timed out")), 60000));
130
+ const { topic } = await Promise.race([
131
+ approvalPromise,
132
+ timeoutPromise,
133
+ ]);
134
+ console.log("WalletConnectV2: Approved session topic", topic);
135
+ // Save this new session to local storage
136
+ const newSession = {
137
+ topic,
138
+ chainIds: [...chainIdsSet],
139
+ };
140
+ localStorage.setItem(this.sessionStorageKey, JSON.stringify(newSession));
141
+ }
142
+ }
143
+ disconnect() {
144
+ const session = localStorage.getItem(this.sessionStorageKey);
145
+ if (session) {
146
+ const { topic } = JSON.parse(session);
147
+ this._disconnect(topic);
148
+ }
149
+ }
150
+ onDisconnect(cb) {
151
+ this.onDisconnectCbs.add(cb);
152
+ return () => {
153
+ this.onDisconnectCbs.delete(cb);
154
+ };
155
+ }
156
+ onAccountChange(cb) {
157
+ this.onAccountChangeCbs.add(cb);
158
+ return () => {
159
+ this.onAccountChangeCbs.delete(cb);
160
+ };
161
+ }
162
+ async getAccount(chainId) {
163
+ if (!this.config?.disableConnectionCheck) {
164
+ const res = await this.request(chainId, Method.GET_ACCOUNTS, {});
165
+ // result might be array or single object depending on wallet impl, usually array
166
+ return Array.isArray(res) ? res[0] : res;
167
+ }
168
+ try {
169
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 3000));
170
+ const resArray = await Promise.race([
171
+ this.request(chainId, Method.GET_ACCOUNTS, {}),
172
+ timeout,
173
+ ]);
174
+ const res = Array.isArray(resArray) ? resArray[0] : resArray;
175
+ // Store successful response
176
+ this.onDisconnect(() => {
177
+ localStorage.removeItem(this.accountStorageKey);
178
+ });
179
+ localStorage.setItem(this.accountStorageKey, JSON.stringify(res));
180
+ return res;
181
+ }
182
+ catch (e) {
183
+ // Try to get stored account data
184
+ const stored = localStorage.getItem(this.accountStorageKey);
185
+ if (stored) {
186
+ const account = JSON.parse(stored);
187
+ // Try to refresh in background
188
+ this.request(chainId, Method.GET_ACCOUNTS, {})
189
+ .then((res) => {
190
+ const accountData = Array.isArray(res) ? res[0] : res;
191
+ localStorage.setItem(this.accountStorageKey, JSON.stringify(accountData));
192
+ })
193
+ .catch(() => { });
194
+ this.onDisconnect(() => {
195
+ localStorage.removeItem(this.accountStorageKey);
196
+ });
197
+ return account;
198
+ }
199
+ throw e;
200
+ }
201
+ }
202
+ async signArbitrary(chainId, signerAddress, data) {
203
+ return this.request(chainId, Method.SIGN_ARBITRARY, {
204
+ chainId,
205
+ signer: signerAddress,
206
+ type: "string",
207
+ data,
208
+ });
209
+ }
210
+ async signAmino(chainId, signerAddress, stdSignDoc) {
211
+ const { signature, signed } = await this.request(chainId, Method.SIGN_AMINO, {
212
+ signerAddress,
213
+ signDoc: stdSignDoc,
214
+ signOptions: DEFAULT_SIGN_OPTIONS,
215
+ });
216
+ return {
217
+ signature: signature,
218
+ signed: signed ?? stdSignDoc,
219
+ };
220
+ }
221
+ async signDirect(chainId, signerAddress, signDoc) {
222
+ const { signature, signed } = await this.request(chainId, Method.SIGN_DIRECT, {
223
+ signerAddress,
224
+ signDoc,
225
+ signOptions: DEFAULT_SIGN_OPTIONS,
226
+ });
227
+ return {
228
+ signature: signature,
229
+ signed: signed ?? signDoc,
230
+ };
231
+ }
232
+ isConnected(signClient, topic, timeoutSeconds) {
233
+ const tryPing = async () => signClient
234
+ .ping({ topic })
235
+ .then(() => true)
236
+ .catch(() => false);
237
+ const waitDisconnect = async () => new Promise((resolve) => {
238
+ // @ts-ignore
239
+ signClient.on("session_delete", (res) => {
240
+ if (topic === res.topic) {
241
+ resolve(false);
242
+ }
243
+ });
244
+ // @ts-ignore
245
+ signClient.on("session_expire", (res) => {
246
+ if (topic === res.topic) {
247
+ resolve(false);
248
+ }
249
+ });
250
+ });
251
+ const timeout = async () => new Promise((resolve) => setTimeout(() => resolve(false), timeoutSeconds * 1000));
252
+ return Promise.race([tryPing(), waitDisconnect(), timeout()]);
253
+ }
254
+ _disconnect(topic) {
255
+ const session = localStorage.getItem(this.sessionStorageKey);
256
+ if (!session || session.includes(topic)) {
257
+ localStorage.removeItem(this.sessionStorageKey);
258
+ this.onDisconnectCbs.forEach((cb) => cb());
259
+ }
260
+ }
261
+ async request(chainId, method, params) {
262
+ const session = localStorage.getItem(this.sessionStorageKey);
263
+ if (!session || !this.signClient) {
264
+ throw new Error("Session not found for " + chainId);
265
+ }
266
+ const { topic } = JSON.parse(session);
267
+ if (isMobile() && method !== Method.GET_ACCOUNTS) {
268
+ window.location.href = isAndroid()
269
+ ? this.mobileAppDetails.android
270
+ : this.mobileAppDetails.ios;
271
+ }
272
+ return this.signClient.request({
273
+ topic,
274
+ chainId: this.toCosmosNamespace(chainId),
275
+ request: {
276
+ method,
277
+ params,
278
+ },
279
+ });
280
+ }
281
+ toCosmosNamespace(chainId) {
282
+ return "cosmos:" + chainId;
283
+ }
284
+ }
@@ -0,0 +1 @@
1
+ export { isAndroid, isIOS, isMobile } from "@goblinhunt/cosmes/wallet";
@@ -0,0 +1 @@
1
+ export { isAndroid, isIOS, isMobile } from "@goblinhunt/cosmes/wallet";
@@ -0,0 +1 @@
1
+ export declare function formatShortAddress(address: string, prefixLen?: number, suffixLen?: number): string;
@@ -0,0 +1,7 @@
1
+ // import { Account } from "./types.js";
2
+ // Currently just a placeholder for potential account-related utilities
3
+ export function formatShortAddress(address, prefixLen = 8, suffixLen = 4) {
4
+ if (!address)
5
+ return "";
6
+ return `${address.slice(0, prefixLen)}...${address.slice(-suffixLen)}`;
7
+ }
@@ -0,0 +1,4 @@
1
+ import { Chain } from "./types.js";
2
+ export declare const TERRA_CLASSIC: Chain;
3
+ export declare const DEFAULT_CHAINS: Chain[];
4
+ export declare function findChain(chains: Chain[], chainId: string): Chain | undefined;
@@ -0,0 +1,11 @@
1
+ export const TERRA_CLASSIC = {
2
+ chainId: "columbus-5",
3
+ rpc: "https://rpc-columbus-5.garuda-defi.org/",
4
+ rest: "https://lcd-columbus-5.garuda-defi.org/",
5
+ bech32Prefix: "terra",
6
+ gasPrice: "28.325uluna", // Approximate gas price for LUNC
7
+ };
8
+ export const DEFAULT_CHAINS = [TERRA_CLASSIC];
9
+ export function findChain(chains, chainId) {
10
+ return chains.find((c) => c.chainId === chainId);
11
+ }
@@ -0,0 +1,2 @@
1
+ import { Client, ClientConfig } from "./types.js";
2
+ export declare function createClient(config: ClientConfig): Client;
@@ -0,0 +1,157 @@
1
+ import { defaultStorage } from "./storage.js";
2
+ const STORAGE_KEY_CHAIN = "cosmos-connect.chain";
3
+ const STORAGE_KEY_WALLET = "cosmos-connect.wallet";
4
+ export function createClient(config) {
5
+ const { chains, wallets } = config;
6
+ const storage = config.storage || defaultStorage;
7
+ let state = {
8
+ currentChain: null,
9
+ currentWallet: null,
10
+ account: null,
11
+ status: "disconnected",
12
+ };
13
+ const listeners = new Set();
14
+ function setState(partial) {
15
+ state = { ...state, ...partial };
16
+ listeners.forEach((listener) => listener(state));
17
+ }
18
+ function getChain(chainId) {
19
+ return chains.find((c) => c.chainId === chainId);
20
+ }
21
+ function getWallet(walletId) {
22
+ return wallets.find((w) => w.id === walletId);
23
+ }
24
+ async function connect(walletId, chainId) {
25
+ try {
26
+ const chain = getChain(chainId);
27
+ if (!chain)
28
+ throw new Error(`Chain ${chainId} not found`);
29
+ const wallet = getWallet(walletId);
30
+ if (!wallet)
31
+ throw new Error(`Wallet ${walletId} not found`);
32
+ setState({ status: "connecting" });
33
+ if (!wallet.installed() && !wallet.getUri) {
34
+ throw new Error(`Wallet ${wallet.name} is not installed`);
35
+ }
36
+ const account = await wallet.connect(chain);
37
+ setState({
38
+ currentChain: chain,
39
+ currentWallet: wallet,
40
+ account,
41
+ status: "connected",
42
+ });
43
+ storage.setItem(STORAGE_KEY_CHAIN, chainId);
44
+ storage.setItem(STORAGE_KEY_WALLET, walletId);
45
+ }
46
+ catch (error) {
47
+ setState({ status: "disconnected", account: null });
48
+ throw error;
49
+ }
50
+ }
51
+ async function disconnect() {
52
+ if (state.currentWallet) {
53
+ try {
54
+ await state.currentWallet.disconnect();
55
+ }
56
+ catch (e) {
57
+ console.error("Wallet disconnect failed", e);
58
+ }
59
+ }
60
+ setState({
61
+ currentChain: null,
62
+ currentWallet: null,
63
+ account: null,
64
+ status: "disconnected",
65
+ });
66
+ storage.removeItem(STORAGE_KEY_CHAIN);
67
+ storage.removeItem(STORAGE_KEY_WALLET);
68
+ }
69
+ async function signAndBroadcast(txBytes) {
70
+ if (!state.currentChain || !state.currentWallet || !state.account) {
71
+ throw new Error("Client not connected");
72
+ }
73
+ // 1. Sign
74
+ const signedTxBytes = await state.currentWallet.signTx(txBytes);
75
+ // 2. Broadcast
76
+ const rpc = state.currentChain.rpc;
77
+ return await broadcastTx(rpc, signedTxBytes);
78
+ }
79
+ function subscribe(listener) {
80
+ listeners.add(listener);
81
+ listener(state);
82
+ return () => {
83
+ listeners.delete(listener);
84
+ };
85
+ }
86
+ if (storage) {
87
+ const savedChainId = storage.getItem(STORAGE_KEY_CHAIN);
88
+ const savedWalletId = storage.getItem(STORAGE_KEY_WALLET);
89
+ if (savedChainId && savedWalletId) {
90
+ connect(savedWalletId, savedChainId).catch(console.error);
91
+ }
92
+ }
93
+ const client = {
94
+ get state() {
95
+ return state;
96
+ },
97
+ connect,
98
+ disconnect,
99
+ signAndBroadcast,
100
+ subscribe,
101
+ getChain,
102
+ getWallet,
103
+ getWallets: () => wallets,
104
+ getChains: () => chains,
105
+ };
106
+ let updateQueued = false;
107
+ // Subscribe to updates from wallets (e.g. for QR code URIs)
108
+ wallets.forEach((w) => {
109
+ if (w.onUpdate) {
110
+ w.onUpdate(() => {
111
+ if (!updateQueued) {
112
+ updateQueued = true;
113
+ queueMicrotask(() => {
114
+ listeners.forEach((l) => l({ ...state }));
115
+ updateQueued = false;
116
+ });
117
+ }
118
+ });
119
+ }
120
+ });
121
+ return client;
122
+ }
123
+ // Browser-compatible base64 encoding
124
+ function toBase64(bytes) {
125
+ let binary = "";
126
+ const len = bytes.byteLength;
127
+ for (let i = 0; i < len; i++) {
128
+ binary += String.fromCharCode(bytes[i]);
129
+ }
130
+ return btoa(binary);
131
+ }
132
+ async function broadcastTx(rpc, signedTx) {
133
+ const txBytesBase64 = toBase64(signedTx);
134
+ const body = {
135
+ jsonrpc: "2.0",
136
+ id: "1",
137
+ method: "broadcast_tx_sync",
138
+ params: {
139
+ tx: txBytesBase64,
140
+ },
141
+ };
142
+ if (typeof fetch === "undefined") {
143
+ throw new Error("Fetch is not defined in this environment");
144
+ }
145
+ const res = await fetch(rpc, {
146
+ method: "POST",
147
+ body: JSON.stringify(body),
148
+ });
149
+ if (!res.ok) {
150
+ throw new Error(`RPC request failed with status ${res.status}`);
151
+ }
152
+ const json = await res.json();
153
+ if (json.error) {
154
+ throw new Error(json.error.message);
155
+ }
156
+ return json.result.hash;
157
+ }
@@ -0,0 +1,2 @@
1
+ import { StorageAdapter } from "./types.js";
2
+ export declare const defaultStorage: StorageAdapter;
@@ -0,0 +1,18 @@
1
+ export const defaultStorage = {
2
+ getItem: (key) => {
3
+ if (typeof localStorage !== "undefined") {
4
+ return localStorage.getItem(key);
5
+ }
6
+ return null;
7
+ },
8
+ setItem: (key, value) => {
9
+ if (typeof localStorage !== "undefined") {
10
+ localStorage.setItem(key, value);
11
+ }
12
+ },
13
+ removeItem: (key) => {
14
+ if (typeof localStorage !== "undefined") {
15
+ localStorage.removeItem(key);
16
+ }
17
+ },
18
+ };
@@ -0,0 +1,55 @@
1
+ export interface Chain {
2
+ chainId: string;
3
+ rpc: string;
4
+ rest?: string;
5
+ bech32Prefix: string;
6
+ gasPrice?: string;
7
+ }
8
+ export interface Account {
9
+ address: string;
10
+ pubKey: Uint8Array;
11
+ algo?: string;
12
+ name?: string;
13
+ isNanoLedger?: boolean;
14
+ }
15
+ export interface WalletAdapter {
16
+ id: string;
17
+ name: string;
18
+ icon?: string;
19
+ installed(): boolean;
20
+ connect(chain: Chain): Promise<Account>;
21
+ disconnect(): Promise<void>;
22
+ signTx(bytes: Uint8Array): Promise<Uint8Array>;
23
+ signMsg?(msg: string): Promise<Uint8Array>;
24
+ getUri?(): string;
25
+ onUpdate?(callback: () => void): void;
26
+ }
27
+ export type ClientStatus = "disconnected" | "connecting" | "connected";
28
+ export interface ClientState {
29
+ currentChain: Chain | null;
30
+ currentWallet: WalletAdapter | null;
31
+ account: Account | null;
32
+ status: ClientStatus;
33
+ }
34
+ export interface ClientConfig {
35
+ chains: Chain[];
36
+ wallets: WalletAdapter[];
37
+ storage?: StorageAdapter;
38
+ }
39
+ export interface StorageAdapter {
40
+ getItem(key: string): string | null;
41
+ setItem(key: string, value: string): void;
42
+ removeItem(key: string): void;
43
+ }
44
+ export type Listener<T> = (state: T) => void;
45
+ export interface Client {
46
+ readonly state: ClientState;
47
+ connect(walletId: string, chainId: string): Promise<void>;
48
+ disconnect(): Promise<void>;
49
+ signAndBroadcast(txBytes: Uint8Array): Promise<string>;
50
+ subscribe(listener: Listener<ClientState>): () => void;
51
+ getChain(chainId: string): Chain | undefined;
52
+ getWallet(walletId: string): WalletAdapter | undefined;
53
+ getWallets(): WalletAdapter[];
54
+ getChains(): Chain[];
55
+ }
@@ -0,0 +1 @@
1
+ export {};