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.
- package/dist/adapters/CosmostationWallet.d.ts +40 -0
- package/dist/adapters/CosmostationWallet.js +68 -0
- package/dist/adapters/GalaxyStationWallet.d.ts +34 -0
- package/dist/adapters/GalaxyStationWallet.js +64 -0
- package/dist/adapters/KeplrWallet.d.ts +36 -0
- package/dist/adapters/KeplrWallet.js +85 -0
- package/dist/adapters/LUNCDashWallet.d.ts +6 -0
- package/dist/adapters/LUNCDashWallet.js +19 -0
- package/dist/adapters/LeapWallet.d.ts +33 -0
- package/dist/adapters/LeapWallet.js +64 -0
- package/dist/adapters/StationWallet.d.ts +34 -0
- package/dist/adapters/StationWallet.js +67 -0
- package/dist/adapters/WalletConnectWallet.d.ts +27 -0
- package/dist/adapters/WalletConnectWallet.js +89 -0
- package/dist/adapters/utils/QRCodeModal.d.ts +20 -0
- package/dist/adapters/utils/QRCodeModal.js +50 -0
- package/dist/adapters/utils/WalletConnectV2.d.ts +52 -0
- package/dist/adapters/utils/WalletConnectV2.js +284 -0
- package/dist/adapters/utils/os.d.ts +1 -0
- package/dist/adapters/utils/os.js +1 -0
- package/dist/core/account.d.ts +1 -0
- package/dist/core/account.js +7 -0
- package/dist/core/chain.d.ts +4 -0
- package/dist/core/chain.js +11 -0
- package/dist/core/createClient.d.ts +2 -0
- package/dist/core/createClient.js +157 -0
- package/dist/core/storage.d.ts +2 -0
- package/dist/core/storage.js +18 -0
- package/dist/core/types.d.ts +55 -0
- package/dist/core/types.js +1 -0
- package/dist/core/wallet.d.ts +2 -0
- package/dist/core/wallet.js +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/package.json +18 -0
- package/src/adapters/CosmostationWallet.ts +113 -0
- package/src/adapters/GalaxyStationWallet.ts +100 -0
- package/src/adapters/KeplrWallet.ts +126 -0
- package/src/adapters/LUNCDashWallet.ts +20 -0
- package/src/adapters/LeapWallet.ts +95 -0
- package/src/adapters/StationWallet.ts +100 -0
- package/src/adapters/WalletConnectWallet.ts +125 -0
- package/src/adapters/utils/QRCodeModal.ts +73 -0
- package/src/adapters/utils/WalletConnectV2.ts +388 -0
- package/src/adapters/utils/os.ts +1 -0
- package/src/core/account.ts +7 -0
- package/src/core/chain.ts +15 -0
- package/src/core/createClient.ts +194 -0
- package/src/core/storage.ts +20 -0
- package/src/core/types.ts +72 -0
- package/src/core/wallet.ts +5 -0
- package/src/index.ts +11 -0
- package/src/types/modules.d.ts +6 -0
- 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,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,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,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 {};
|