@rango-dev/provider-walletconnect-2 0.1.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/dist/constants.d.ts +53 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/helpers.d.ts +20 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/session.d.ts +63 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/signer.d.ts +4 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signers/cosmos.d.ts +16 -0
- package/dist/signers/cosmos.d.ts.map +1 -0
- package/dist/signers/evm.d.ts +16 -0
- package/dist/signers/evm.d.ts.map +1 -0
- package/dist/signers/helper.d.ts +3 -0
- package/dist/signers/helper.d.ts.map +1 -0
- package/dist/signers/mock.d.ts +3 -0
- package/dist/signers/mock.d.ts.map +1 -0
- package/dist/signers/solana.d.ts +15 -0
- package/dist/signers/solana.d.ts.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +41 -0
- package/readme.md +10 -0
- package/src/constants.ts +83 -0
- package/src/helpers.ts +165 -0
- package/src/index.ts +177 -0
- package/src/session.ts +308 -0
- package/src/signer.ts +32 -0
- package/src/signers/cosmos.ts +243 -0
- package/src/signers/evm.ts +134 -0
- package/src/signers/helper.ts +73 -0
- package/src/signers/mock.ts +3675 -0
- package/src/signers/solana.ts +155 -0
- package/src/types.ts +25 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { AccountId } from 'caip';
|
|
2
|
+
import { Networks, timeout } from '@rango-dev/wallets-shared';
|
|
3
|
+
import type { SignClient } from '@walletconnect/sign-client/dist/types/client';
|
|
4
|
+
import {
|
|
5
|
+
PairingTypes,
|
|
6
|
+
SessionTypes,
|
|
7
|
+
SignClientTypes,
|
|
8
|
+
} from '@walletconnect/types';
|
|
9
|
+
import { Web3Modal } from '@web3modal/standalone';
|
|
10
|
+
import { getSdkError } from '@walletconnect/utils';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
generateOptionalNamespace,
|
|
14
|
+
generateRequiredNamespace,
|
|
15
|
+
getChainIdByNetworkName,
|
|
16
|
+
solanaChainIdToNetworkName,
|
|
17
|
+
} from './helpers';
|
|
18
|
+
import { PING_TIMEOUT, PROJECT_ID } from './constants';
|
|
19
|
+
import { ConnectParams, CreateSessionParams, WCInstance } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a Web3Modal instance
|
|
23
|
+
*/
|
|
24
|
+
const web3Modal = new Web3Modal({
|
|
25
|
+
projectId: PROJECT_ID,
|
|
26
|
+
themeMode: 'light',
|
|
27
|
+
walletConnectVersion: 2,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function getLastSession(client: SignClient) {
|
|
31
|
+
return client.session.values[client.session.values.length - 1];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
* Try to ping the wallet, if wallet responded with `pong`, session is a valid and we will use the session.
|
|
37
|
+
* If the wallet didn't respond during 10 seconds (PING_TIME), we assume the wallet isn't available and we need to create a new session.
|
|
38
|
+
*
|
|
39
|
+
*/
|
|
40
|
+
export async function restoreSession(
|
|
41
|
+
client: SignClient,
|
|
42
|
+
pairing: PairingTypes.Struct
|
|
43
|
+
): Promise<SessionTypes.Struct | undefined> {
|
|
44
|
+
await timeout(
|
|
45
|
+
client.ping({
|
|
46
|
+
topic: pairing.topic,
|
|
47
|
+
}),
|
|
48
|
+
PING_TIMEOUT
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// We assume last session is the correct session, beacuse we are doing clean up and keeps only one pairing/session.
|
|
52
|
+
const session = getLastSession(client);
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* Getting a pair of required and optional namespaces then tries to show a modal and connect (pair)
|
|
59
|
+
* To the wallet.
|
|
60
|
+
* @param client
|
|
61
|
+
* @param options
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
export async function createSession(
|
|
65
|
+
client: SignClient,
|
|
66
|
+
options: CreateSessionParams
|
|
67
|
+
): Promise<SessionTypes.Struct> {
|
|
68
|
+
const { requiredNamespaces, optionalNamespaces, pairingTopic } = options;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const { uri, approval } = await client.connect({
|
|
72
|
+
requiredNamespaces,
|
|
73
|
+
optionalNamespaces,
|
|
74
|
+
pairingTopic,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Open QRCode modal if a URI was returned (i.e. we're not connecting an existing pairing).
|
|
78
|
+
if (uri) {
|
|
79
|
+
// Create a flat array of all requested chains across namespaces.
|
|
80
|
+
const allNamespaces = {
|
|
81
|
+
...(requiredNamespaces || {}),
|
|
82
|
+
...(optionalNamespaces || {}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const standaloneChains = Object.values(allNamespaces)
|
|
86
|
+
.map((namespace) => namespace.chains)
|
|
87
|
+
.flat() as string[];
|
|
88
|
+
|
|
89
|
+
web3Modal.openModal({ uri, standaloneChains });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const session = await approval();
|
|
93
|
+
return session;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(e);
|
|
96
|
+
throw e;
|
|
97
|
+
} finally {
|
|
98
|
+
web3Modal.closeModal();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
*
|
|
104
|
+
* A user (client) can have multiple pairings (to different wallets), we are assuming
|
|
105
|
+
* the last pairing is the active pairing for now. A better UX can be showing a list of pairings
|
|
106
|
+
* and let the user to choose the right pairing manually. Because we don't have that yet, we will pick up the last one.
|
|
107
|
+
*
|
|
108
|
+
*/
|
|
109
|
+
export function tryGetPairing(
|
|
110
|
+
client: SignClient
|
|
111
|
+
): PairingTypes.Struct | undefined {
|
|
112
|
+
const pairings = client.pairing.getAll({ active: true });
|
|
113
|
+
const lastPairing =
|
|
114
|
+
pairings.length > 0 ? pairings[pairings.length - 1] : undefined;
|
|
115
|
+
|
|
116
|
+
return lastPairing;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
*
|
|
121
|
+
* Try to restore the session first, if couldn't, create a new session by showing a modal.
|
|
122
|
+
*
|
|
123
|
+
*/
|
|
124
|
+
export async function tryConnect(
|
|
125
|
+
client: SignClient,
|
|
126
|
+
params: ConnectParams
|
|
127
|
+
): Promise<SessionTypes.Struct> {
|
|
128
|
+
const { network, meta } = params;
|
|
129
|
+
|
|
130
|
+
const requiredNamespaces = generateRequiredNamespace(meta, network);
|
|
131
|
+
/**
|
|
132
|
+
* We try to get all of our supported chains as optional.
|
|
133
|
+
* Currently, it only works on Trust Wallet (Note: the response is buggy and only returns eip155 optional namespaces).
|
|
134
|
+
*/
|
|
135
|
+
const optionalNamespaces = generateOptionalNamespace(meta);
|
|
136
|
+
|
|
137
|
+
if (!requiredNamespaces) {
|
|
138
|
+
throw new Error(`Couldn't generate required namespace for ${network}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if the user has a session, if yes, restore the session and use it.
|
|
142
|
+
let session: SessionTypes.Struct | undefined;
|
|
143
|
+
const pairing = tryGetPairing(client);
|
|
144
|
+
if (pairing) {
|
|
145
|
+
try {
|
|
146
|
+
session = await restoreSession(client, pairing);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
await disconnectSessions(client);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// In case of connecting for the first time or session couldn't be restored, we will create a new session.
|
|
153
|
+
if (!session) {
|
|
154
|
+
session = await createSession(client, {
|
|
155
|
+
requiredNamespaces,
|
|
156
|
+
optionalNamespaces,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return session;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Wallet connect is a multichain protocol and we can not determine the connected wallet
|
|
165
|
+
* supports which wallet, `extend`ing session doesn't work during a bug in their utils packages.
|
|
166
|
+
* So we will try to make a new session with `network` that user needs to switch.
|
|
167
|
+
*/
|
|
168
|
+
export async function trySwitchByCreatingNewSession(
|
|
169
|
+
instance: WCInstance,
|
|
170
|
+
params: ConnectParams
|
|
171
|
+
): Promise<SessionTypes.Struct> {
|
|
172
|
+
const { client, session } = instance;
|
|
173
|
+
const { network, meta } = params;
|
|
174
|
+
|
|
175
|
+
if (!session) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
'For switching network, you need to have an active session!'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If a session has the chain id in its namespace, we can use that session.
|
|
182
|
+
const chainId = getChainIdByNetworkName(network, params.meta) || network;
|
|
183
|
+
const requestedSession = getSessionByChainId(client, chainId);
|
|
184
|
+
if (requestedSession) {
|
|
185
|
+
return requestedSession;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Creating a new session for requested network.
|
|
189
|
+
const requiredNamespaces = generateRequiredNamespace(meta, network);
|
|
190
|
+
if (!requiredNamespaces) {
|
|
191
|
+
throw new Error(`Couldn't generate requiredNamespaces for ${network}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const createdSession = await createSession(client, { requiredNamespaces });
|
|
195
|
+
|
|
196
|
+
return createdSession;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
*
|
|
201
|
+
* Looking for a chainId in sessions and return the session if found.
|
|
202
|
+
*
|
|
203
|
+
*/
|
|
204
|
+
function getSessionByChainId(
|
|
205
|
+
client: WCInstance['client'],
|
|
206
|
+
chainId: string
|
|
207
|
+
): SessionTypes.Struct | undefined {
|
|
208
|
+
const sessions = client.session.getAll();
|
|
209
|
+
const requestedSession = sessions.find((session) => {
|
|
210
|
+
const accounts = getAccountsFromSession(session);
|
|
211
|
+
return accounts.find((account) => account.chainId === chainId);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return requestedSession;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
*
|
|
219
|
+
* Try to find sessions with a topic id and expire them.
|
|
220
|
+
*
|
|
221
|
+
*/
|
|
222
|
+
export async function cleanupSingleSession(client: SignClient, topic: string) {
|
|
223
|
+
const sessions = client.session.getAll();
|
|
224
|
+
const pairings = client.pairing.getAll();
|
|
225
|
+
|
|
226
|
+
sessions.forEach((session) => {
|
|
227
|
+
if (session.topic === topic || session.pairingTopic === topic) {
|
|
228
|
+
const requestForDeleteTopic =
|
|
229
|
+
session.pairingTopic === topic ? session.pairingTopic : session.topic;
|
|
230
|
+
client.core.expirer.set(requestForDeleteTopic, 0);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
pairings.forEach((pairing) => {
|
|
235
|
+
if (pairing.topic === topic) {
|
|
236
|
+
client.core.expirer.set(topic, 0);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
*
|
|
243
|
+
* Disconnect means to delete the session on both parties (dApp & wallet) at the same time.
|
|
244
|
+
*
|
|
245
|
+
*/
|
|
246
|
+
export async function disconnectSessions(client: SignClient) {
|
|
247
|
+
const allPromises = [];
|
|
248
|
+
|
|
249
|
+
const sessions = client.session.getAll();
|
|
250
|
+
for (const session of sessions) {
|
|
251
|
+
allPromises.push(
|
|
252
|
+
client.disconnect({
|
|
253
|
+
topic: session.topic,
|
|
254
|
+
reason: getSdkError('USER_DISCONNECTED'),
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const pairings = client.pairing.getAll();
|
|
260
|
+
for (const pairing of pairings) {
|
|
261
|
+
allPromises.push(
|
|
262
|
+
client.disconnect({
|
|
263
|
+
topic: pairing.topic,
|
|
264
|
+
reason: getSdkError('USER_DISCONNECTED'),
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return await Promise.all(allPromises);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getAccountsFromSession(session: SessionTypes.Struct) {
|
|
273
|
+
const accounts = Object.values(session.namespaces)
|
|
274
|
+
.map((namespace) => namespace.accounts)
|
|
275
|
+
.flat()
|
|
276
|
+
.map((account) => {
|
|
277
|
+
const { address, chainId } = new AccountId(account);
|
|
278
|
+
// Note: Solana has a specific ID, we need to convert it back to network name.
|
|
279
|
+
// It will return the chain id itslef if it's not that specific ID.
|
|
280
|
+
const chain = solanaChainIdToNetworkName(chainId.reference);
|
|
281
|
+
return {
|
|
282
|
+
accounts: [address],
|
|
283
|
+
chainId: chain,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return accounts;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function getAccountsFromEvent(
|
|
291
|
+
event: SignClientTypes.BaseEventArgs<{
|
|
292
|
+
namespaces: SessionTypes.Namespaces;
|
|
293
|
+
}>
|
|
294
|
+
) {
|
|
295
|
+
const accounts = Object.values(event.params.namespaces)
|
|
296
|
+
.map((namespace) => namespace.accounts)
|
|
297
|
+
.flat()
|
|
298
|
+
.map((account) => {
|
|
299
|
+
const { address, chainId } = new AccountId(account);
|
|
300
|
+
return {
|
|
301
|
+
accounts: [address],
|
|
302
|
+
chainId:
|
|
303
|
+
chainId.namespace === 'solana' ? Networks.SOLANA : chainId.reference,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return accounts;
|
|
308
|
+
}
|
package/src/signer.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultSignerFactory,
|
|
3
|
+
SignerFactory,
|
|
4
|
+
TransactionType as TxType,
|
|
5
|
+
} from 'rango-types';
|
|
6
|
+
|
|
7
|
+
import { WCInstance } from './types';
|
|
8
|
+
import EVMSigner from './signers/evm';
|
|
9
|
+
import COSMOSSigner from './signers/cosmos';
|
|
10
|
+
import SOLANASigner from './signers/solana';
|
|
11
|
+
|
|
12
|
+
export default function getSigners(instance: WCInstance): SignerFactory {
|
|
13
|
+
if (!instance.session) {
|
|
14
|
+
throw new Error('Session is required for wallet connect signers.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const signers = new DefaultSignerFactory();
|
|
18
|
+
signers.registerSigner(
|
|
19
|
+
TxType.EVM,
|
|
20
|
+
new EVMSigner(instance.client, instance.session)
|
|
21
|
+
);
|
|
22
|
+
signers.registerSigner(
|
|
23
|
+
TxType.COSMOS,
|
|
24
|
+
new COSMOSSigner(instance.client, instance.session)
|
|
25
|
+
);
|
|
26
|
+
signers.registerSigner(
|
|
27
|
+
TxType.SOLANA,
|
|
28
|
+
new SOLANASigner(instance.client, instance.session)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return signers;
|
|
32
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { SignClient } from '@walletconnect/sign-client/dist/types/client';
|
|
2
|
+
import { SessionTypes } from '@walletconnect/types';
|
|
3
|
+
import { AccountId, ChainId } from 'caip';
|
|
4
|
+
import type { GenericSigner } from 'rango-types';
|
|
5
|
+
import { SignerError, CosmosTransaction, SignerErrorCode } from 'rango-types';
|
|
6
|
+
import { CosmosRPCMethods, NAMESPACES } from '../constants';
|
|
7
|
+
import { getsignedTx, manipulateMsg } from '@rango-dev/signer-cosmos';
|
|
8
|
+
import {
|
|
9
|
+
AminoSignResponse,
|
|
10
|
+
BroadcastMode,
|
|
11
|
+
makeSignDoc,
|
|
12
|
+
} from '@cosmjs/launchpad';
|
|
13
|
+
import { sendTx } from './helper';
|
|
14
|
+
import { supportedChains } from './mock';
|
|
15
|
+
import { uint8ArrayToHex } from '@rango-dev/wallets-shared';
|
|
16
|
+
import { cosmos } from '@keplr-wallet/cosmos';
|
|
17
|
+
import { formatDirectSignDoc, stringifySignDocValues } from 'cosmos-wallet';
|
|
18
|
+
|
|
19
|
+
const NAMESPACE_NAME = NAMESPACES.COSMOS;
|
|
20
|
+
type DirectSignResponse = {
|
|
21
|
+
signature: {
|
|
22
|
+
pub_key: {
|
|
23
|
+
type: string;
|
|
24
|
+
value: string;
|
|
25
|
+
};
|
|
26
|
+
signature: string;
|
|
27
|
+
};
|
|
28
|
+
signed: {
|
|
29
|
+
chainId: string;
|
|
30
|
+
accountNumber: string;
|
|
31
|
+
authInfoBytes: string;
|
|
32
|
+
bodyBytes: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
class COSMOSSigner implements GenericSigner<CosmosTransaction> {
|
|
36
|
+
private client: SignClient;
|
|
37
|
+
private session: SessionTypes.Struct;
|
|
38
|
+
|
|
39
|
+
constructor(client: SignClient, session: SessionTypes.Struct) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.session = session;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async signMessage(): Promise<string> {
|
|
45
|
+
throw SignerError.UnimplementedError('signMessage');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async signAndSendTx(
|
|
49
|
+
tx: CosmosTransaction,
|
|
50
|
+
address: string,
|
|
51
|
+
chainId: string | null
|
|
52
|
+
): Promise<{ hash: string }> {
|
|
53
|
+
console.log({ tx, address, chainId });
|
|
54
|
+
|
|
55
|
+
const requestedFor = this.isNetworkAndAccountExistInSession({
|
|
56
|
+
address,
|
|
57
|
+
chainId,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const { memo, sequence, account_number, chainId, msgs, fee, signType } =
|
|
61
|
+
tx.data;
|
|
62
|
+
const msgsWithoutType = msgs.map((m) => ({
|
|
63
|
+
...manipulateMsg(m),
|
|
64
|
+
__type: undefined,
|
|
65
|
+
'@type': undefined,
|
|
66
|
+
}));
|
|
67
|
+
if (!chainId)
|
|
68
|
+
throw SignerError.AssertionFailed('chainId is undefined from server');
|
|
69
|
+
if (!account_number)
|
|
70
|
+
throw SignerError.AssertionFailed(
|
|
71
|
+
'account_number is undefined from server'
|
|
72
|
+
);
|
|
73
|
+
if (!sequence)
|
|
74
|
+
throw SignerError.AssertionFailed('sequence is undefined from server');
|
|
75
|
+
|
|
76
|
+
if (signType === 'AMINO') {
|
|
77
|
+
const signDoc = makeSignDoc(
|
|
78
|
+
msgsWithoutType as any,
|
|
79
|
+
fee as any,
|
|
80
|
+
chainId,
|
|
81
|
+
memo || undefined,
|
|
82
|
+
account_number,
|
|
83
|
+
sequence
|
|
84
|
+
);
|
|
85
|
+
let signResponse;
|
|
86
|
+
try {
|
|
87
|
+
signResponse = await this.client.request<AminoSignResponse>({
|
|
88
|
+
topic: this.session.topic,
|
|
89
|
+
chainId: requestedFor.caipChainId,
|
|
90
|
+
request: {
|
|
91
|
+
method: CosmosRPCMethods.SIGN_AMINO,
|
|
92
|
+
params: {
|
|
93
|
+
signDoc,
|
|
94
|
+
signerAddress: tx.fromWalletAddress,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, err);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const signedTx = getsignedTx(tx, signResponse);
|
|
103
|
+
const result = await sendTx(
|
|
104
|
+
chainId,
|
|
105
|
+
signedTx,
|
|
106
|
+
BroadcastMode.Async,
|
|
107
|
+
supportedChains
|
|
108
|
+
);
|
|
109
|
+
return { hash: uint8ArrayToHex(result) };
|
|
110
|
+
} else if (signType === 'DIRECT') {
|
|
111
|
+
let getAccounts;
|
|
112
|
+
try {
|
|
113
|
+
getAccounts = await this.client.request<
|
|
114
|
+
Array<{
|
|
115
|
+
address: string;
|
|
116
|
+
algo: string;
|
|
117
|
+
pubkey: string;
|
|
118
|
+
}>
|
|
119
|
+
>({
|
|
120
|
+
topic: this.session.topic,
|
|
121
|
+
chainId: requestedFor.caipChainId,
|
|
122
|
+
request: {
|
|
123
|
+
method: CosmosRPCMethods.GET_ACCOUNTS,
|
|
124
|
+
params: {},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, err);
|
|
129
|
+
}
|
|
130
|
+
const pubkey =
|
|
131
|
+
getAccounts?.find(
|
|
132
|
+
(account) => account.address === tx.fromWalletAddress
|
|
133
|
+
)?.pubkey || '';
|
|
134
|
+
|
|
135
|
+
const bodyBytes = cosmos.tx.v1beta1.TxBody.encode({
|
|
136
|
+
messages: tx.data.protoMsgs.map((m) => ({
|
|
137
|
+
type_url: m.type_url,
|
|
138
|
+
value: new Uint8Array(m.value),
|
|
139
|
+
})),
|
|
140
|
+
memo,
|
|
141
|
+
}).finish();
|
|
142
|
+
console.log({ tx, bodyBytes });
|
|
143
|
+
|
|
144
|
+
const signDoc = formatDirectSignDoc(
|
|
145
|
+
fee?.amount || [],
|
|
146
|
+
pubkey,
|
|
147
|
+
parseInt(fee?.gas as string),
|
|
148
|
+
account_number,
|
|
149
|
+
parseInt(sequence),
|
|
150
|
+
uint8ArrayToHex(bodyBytes),
|
|
151
|
+
chainId
|
|
152
|
+
);
|
|
153
|
+
let signResponse;
|
|
154
|
+
try {
|
|
155
|
+
signResponse = await this.client.request<DirectSignResponse>({
|
|
156
|
+
topic: this.session.topic,
|
|
157
|
+
chainId: requestedFor.caipChainId,
|
|
158
|
+
request: {
|
|
159
|
+
method: CosmosRPCMethods.SIGN_DIRECT,
|
|
160
|
+
params: {
|
|
161
|
+
signDoc: stringifySignDocValues(signDoc),
|
|
162
|
+
signerAddress: tx.fromWalletAddress,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, err);
|
|
168
|
+
}
|
|
169
|
+
console.log({ signResponse });
|
|
170
|
+
const signedTx = cosmos.tx.v1beta1.TxRaw.encode({
|
|
171
|
+
bodyBytes: new TextEncoder().encode(signResponse.signed.bodyBytes),
|
|
172
|
+
authInfoBytes: new TextEncoder().encode(
|
|
173
|
+
signResponse.signed.authInfoBytes
|
|
174
|
+
),
|
|
175
|
+
signatures: [Buffer.from(signResponse.signature.signature, 'base64')],
|
|
176
|
+
}).finish();
|
|
177
|
+
console.log({ signedTx });
|
|
178
|
+
const result = await sendTx(
|
|
179
|
+
chainId,
|
|
180
|
+
signedTx,
|
|
181
|
+
BroadcastMode.Async,
|
|
182
|
+
supportedChains
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
console.log({ result });
|
|
186
|
+
|
|
187
|
+
return { hash: uint8ArrayToHex(result) };
|
|
188
|
+
} else {
|
|
189
|
+
throw new SignerError(
|
|
190
|
+
SignerErrorCode.OPERATION_UNSUPPORTED,
|
|
191
|
+
`Sign type for cosmos not supported, type: ${signType}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (SignerError.isSignerError(err)) throw err;
|
|
196
|
+
else throw new SignerError(SignerErrorCode.SEND_TX_ERROR, undefined, err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private isNetworkAndAccountExistInSession(requestedFor: {
|
|
201
|
+
address: string;
|
|
202
|
+
chainId: string | null;
|
|
203
|
+
}) {
|
|
204
|
+
const { address, chainId } = requestedFor;
|
|
205
|
+
|
|
206
|
+
if (!chainId) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
'You need to set your chain for signing message/transaction.'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const caipAddress = new AccountId({
|
|
213
|
+
chainId: {
|
|
214
|
+
namespace: NAMESPACE_NAME,
|
|
215
|
+
reference: chainId,
|
|
216
|
+
},
|
|
217
|
+
address,
|
|
218
|
+
});
|
|
219
|
+
const addresses = this.session.namespaces[NAMESPACE_NAME]?.accounts.map(
|
|
220
|
+
(address) => address.toLowerCase()
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!addresses || !addresses.includes(caipAddress.toString())) {
|
|
224
|
+
console.warn(
|
|
225
|
+
'Available adresses and requested address:',
|
|
226
|
+
addresses,
|
|
227
|
+
caipAddress.toString()
|
|
228
|
+
);
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Your requested address doesn't exist on your wallect connect session. Please reconnect your wallet.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const caipChainId = new ChainId({
|
|
235
|
+
namespace: NAMESPACE_NAME,
|
|
236
|
+
reference: chainId,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return { chainId, address, caipChainId: caipChainId.toString() };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default COSMOSSigner;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SignClient } from '@walletconnect/sign-client/dist/types/client';
|
|
2
|
+
import { SessionTypes } from '@walletconnect/types';
|
|
3
|
+
import { AccountId, ChainId } from 'caip';
|
|
4
|
+
import type { GenericSigner } from 'rango-types';
|
|
5
|
+
import { EvmTransaction } from 'rango-types/lib/api/main';
|
|
6
|
+
import * as encoding from '@walletconnect/encoding';
|
|
7
|
+
import { EthereumRPCMethods, NAMESPACES } from '../constants';
|
|
8
|
+
|
|
9
|
+
const NAMESPACE_NAME = NAMESPACES.ETHEREUM;
|
|
10
|
+
|
|
11
|
+
class EVMSigner implements GenericSigner<EvmTransaction> {
|
|
12
|
+
private client: SignClient;
|
|
13
|
+
private session: SessionTypes.Struct;
|
|
14
|
+
|
|
15
|
+
constructor(client: SignClient, session: SessionTypes.Struct) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.session = session;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async signMessage(
|
|
21
|
+
msg: string,
|
|
22
|
+
address: string,
|
|
23
|
+
chainId: string | null
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const requestedFor = this.isNetworkAndAccountExistInSession({
|
|
26
|
+
address,
|
|
27
|
+
chainId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const caipChainId = new ChainId({
|
|
31
|
+
namespace: NAMESPACE_NAME,
|
|
32
|
+
reference: requestedFor.chainId,
|
|
33
|
+
});
|
|
34
|
+
const hexMsg = encoding.utf8ToHex(msg, true);
|
|
35
|
+
|
|
36
|
+
const params = [hexMsg, address];
|
|
37
|
+
|
|
38
|
+
// Send message to wallet (using relayer)
|
|
39
|
+
const signature: string = await this.client.request({
|
|
40
|
+
topic: this.session.topic,
|
|
41
|
+
chainId: caipChainId.toString(),
|
|
42
|
+
request: {
|
|
43
|
+
method: EthereumRPCMethods.PERSONAL_SIGN,
|
|
44
|
+
params,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// TODO: We can also verify the signature here
|
|
49
|
+
// Check web-examples: dapps/react-dapp-v2/src/contexts/JsonRpcContext.tsx
|
|
50
|
+
|
|
51
|
+
return signature;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async signAndSendTx(
|
|
55
|
+
tx: EvmTransaction,
|
|
56
|
+
address: string,
|
|
57
|
+
chainId: string | null
|
|
58
|
+
): Promise<{ hash: string }> {
|
|
59
|
+
const requestedFor = this.isNetworkAndAccountExistInSession({
|
|
60
|
+
address,
|
|
61
|
+
chainId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const signedTx: string = await this.client.request({
|
|
65
|
+
topic: this.session.topic,
|
|
66
|
+
chainId: requestedFor.caipChainId,
|
|
67
|
+
request: {
|
|
68
|
+
method: EthereumRPCMethods.SEND_TRANSACTION,
|
|
69
|
+
params: [tx],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
hash: signedTx,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private isNetworkAndAccountExistInSession(requestedFor: {
|
|
79
|
+
address: string;
|
|
80
|
+
chainId: string | null;
|
|
81
|
+
}) {
|
|
82
|
+
const { address, chainId } = requestedFor;
|
|
83
|
+
|
|
84
|
+
if (!chainId) {
|
|
85
|
+
console.log('isNetworkAndAccountExistInSession', requestedFor);
|
|
86
|
+
throw new Error(
|
|
87
|
+
'You need to set your chain for signing message/transaction.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// TODO: We need to make sure we are using a single format for chain ids, it should be hex or number.
|
|
92
|
+
// This is a quick fix for evm.
|
|
93
|
+
const chainIdNumber = chainId.startsWith('0x')
|
|
94
|
+
? String(parseInt(chainId))
|
|
95
|
+
: chainId;
|
|
96
|
+
|
|
97
|
+
const caipAddress = new AccountId({
|
|
98
|
+
chainId: {
|
|
99
|
+
namespace: NAMESPACE_NAME,
|
|
100
|
+
reference: chainIdNumber,
|
|
101
|
+
},
|
|
102
|
+
address,
|
|
103
|
+
});
|
|
104
|
+
const addresses = this.session.namespaces[NAMESPACE_NAME]?.accounts.map(
|
|
105
|
+
(address) => address.toLowerCase()
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!addresses || !addresses.includes(caipAddress.toString())) {
|
|
109
|
+
console.warn(
|
|
110
|
+
'Available adresses and requested address:',
|
|
111
|
+
addresses,
|
|
112
|
+
caipAddress.toString(),
|
|
113
|
+
chainId,
|
|
114
|
+
address
|
|
115
|
+
);
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Your requested address doesn't exist on your wallect connect session. Please reconnect your wallet.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const caipChainId = new ChainId({
|
|
122
|
+
namespace: NAMESPACE_NAME,
|
|
123
|
+
reference: chainIdNumber,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
chainId: chainIdNumber,
|
|
128
|
+
address,
|
|
129
|
+
caipChainId: caipChainId.toString(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default EVMSigner;
|