Skip to content

Commit 792fbad

Browse files
committed
refactor: remove dependency to legacy provider on get signers
1 parent 88b2b3a commit 792fbad

File tree

23 files changed

+591
-9
lines changed

23 files changed

+591
-9
lines changed

signers/signer-evm/src/hub.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import type { EvmActions } from '../../../wallets/core/dist/namespaces/evm/types.js';
2+
import type { ProxiedNamespace } from '@rango-dev/wallets-core';
3+
import type { EvmTransaction } from 'rango-types/mainApi';
4+
5+
import {
6+
isError,
7+
type TransactionRequest,
8+
type TransactionResponse,
9+
} from 'ethers';
10+
import { BrowserProvider } from 'ethers';
11+
import {
12+
type GenericSigner,
13+
RPCErrorCode as RangoRPCErrorCode,
14+
SignerError,
15+
SignerErrorCode,
16+
} from 'rango-types';
17+
18+
import { cleanEvmError, getTenderlyError, waitMs } from './helper.js';
19+
20+
const waitWithMempoolCheck = async (
21+
namespace: ProxiedNamespace<EvmActions>,
22+
tx: TransactionResponse,
23+
txHash: string,
24+
confirmations?: number
25+
) => {
26+
const TIMEOUT = 3_000;
27+
let finished = false;
28+
return await Promise.race([
29+
(async () => {
30+
await tx.wait(confirmations);
31+
finished = true;
32+
})(),
33+
(async () => {
34+
while (!finished) {
35+
await waitMs(TIMEOUT);
36+
if (finished) {
37+
return null;
38+
}
39+
try {
40+
const mempoolTx = await namespace.getTransaction(txHash);
41+
if (!mempoolTx) {
42+
return null;
43+
}
44+
} catch (error) {
45+
console.log({ error });
46+
return null;
47+
}
48+
}
49+
return null;
50+
})(),
51+
]);
52+
};
53+
54+
const checkChainIdChanged = async (
55+
namespace: ProxiedNamespace<EvmActions>,
56+
chainId: string
57+
) => {
58+
const evmInstance = namespace.getInstance();
59+
if (!evmInstance) {
60+
return true;
61+
}
62+
const provider = new BrowserProvider(evmInstance);
63+
const signerChainId = (await provider.getNetwork()).chainId;
64+
if (
65+
!signerChainId ||
66+
Number(chainId).toString() !== signerChainId.toString()
67+
) {
68+
return true;
69+
}
70+
71+
return false;
72+
};
73+
74+
export class HubEvmSigner implements GenericSigner<EvmTransaction> {
75+
private namespace: ProxiedNamespace<EvmActions>;
76+
77+
constructor(namespace: ProxiedNamespace<EvmActions>) {
78+
this.namespace = namespace;
79+
}
80+
81+
static buildTx(evmTx: EvmTransaction, disableV2 = false): TransactionRequest {
82+
const TO_STRING_BASE = 16;
83+
let tx: TransactionRequest = {};
84+
/*
85+
* it's better to pass 0x instead of undefined, otherwise some wallets could face issue
86+
* https://github.com/WalletConnect/web3modal/issues/1082#issuecomment-1637793242
87+
*/
88+
tx = {
89+
data: evmTx.data || '0x',
90+
};
91+
if (evmTx.from) {
92+
tx = { ...tx, from: evmTx.from };
93+
}
94+
if (evmTx.to) {
95+
tx = { ...tx, to: evmTx.to };
96+
}
97+
if (evmTx.value) {
98+
tx = { ...tx, value: evmTx.value };
99+
}
100+
if (evmTx.nonce) {
101+
tx = { ...tx, nonce: parseInt(evmTx.nonce) };
102+
}
103+
if (evmTx.gasLimit) {
104+
tx = { ...tx, gasLimit: evmTx.gasLimit };
105+
}
106+
if (!disableV2 && evmTx.maxFeePerGas && evmTx.maxPriorityFeePerGas) {
107+
tx = {
108+
...tx,
109+
maxFeePerGas: evmTx.maxFeePerGas,
110+
maxPriorityFeePerGas: evmTx.maxPriorityFeePerGas,
111+
};
112+
} else if (evmTx.gasPrice) {
113+
tx = {
114+
...tx,
115+
gasPrice: '0x' + parseInt(evmTx.gasPrice).toString(TO_STRING_BASE),
116+
};
117+
}
118+
return tx;
119+
}
120+
121+
async signMessage(msg: string): Promise<string> {
122+
try {
123+
return this.namespace.signMessage(msg);
124+
} catch (error) {
125+
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, error);
126+
}
127+
}
128+
129+
async signAndSendTx(
130+
tx: EvmTransaction,
131+
address: string,
132+
chainId: string | null
133+
): Promise<{ hash: string; response: TransactionResponse }> {
134+
try {
135+
try {
136+
const transaction = HubEvmSigner.buildTx(tx);
137+
const response = await this.namespace.sendTransaction(
138+
transaction,
139+
address,
140+
chainId
141+
);
142+
return { hash: response.hash, response };
143+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
144+
} catch (error: any) {
145+
// retrying EIP-1559 without v2 related fields
146+
if (
147+
!!error?.message &&
148+
typeof error.message === 'string' &&
149+
error.message.indexOf('EIP-1559') !== -1
150+
) {
151+
console.log('retrying EIP-1559 error without v2 fields ...');
152+
const transaction = HubEvmSigner.buildTx(tx, true);
153+
const response = await this.namespace.sendTransaction(
154+
transaction,
155+
address,
156+
chainId
157+
);
158+
return { hash: response.hash, response };
159+
}
160+
throw error;
161+
}
162+
} catch (error) {
163+
throw cleanEvmError(error);
164+
}
165+
}
166+
167+
async wait(
168+
txHash: string,
169+
chainId?: string,
170+
txResponse?: TransactionResponse,
171+
confirmations?: number
172+
): Promise<{ hash: string; response?: TransactionResponse }> {
173+
try {
174+
/*
175+
* if we have transaction response, use that to wait
176+
* otherwise, try to get tx response from the wallet provider
177+
*/
178+
if (txResponse) {
179+
// if we use waitWithMempoolCheck here, we can't detect replaced tx anymore
180+
await txResponse?.wait(confirmations);
181+
return { hash: txHash };
182+
}
183+
184+
// ignore wait if namespace is not connected yet
185+
if (!this.namespace.state()[0]?.()?.connected) {
186+
return { hash: txHash };
187+
}
188+
189+
// ignore wait if namespace does not support getTransaction
190+
if (!('getTransaction' in this.namespace)) {
191+
return { hash: txHash };
192+
}
193+
194+
/*
195+
* don't proceed if signer chain changed or chain id is not specified
196+
* because if user change the wallet network, we receive null on getTransaction
197+
*/
198+
if (!chainId) {
199+
return { hash: txHash };
200+
}
201+
202+
const hasChainIdChanged = await checkChainIdChanged(
203+
this.namespace,
204+
chainId
205+
);
206+
if (hasChainIdChanged) {
207+
return { hash: txHash };
208+
}
209+
210+
const tx = await this.namespace.getTransaction(txHash);
211+
if (!tx) {
212+
throw Error(`Transaction hash '${txHash}' not found in blockchain.`);
213+
}
214+
215+
await waitWithMempoolCheck(this.namespace, tx, txHash, confirmations);
216+
return { hash: txHash };
217+
} catch (error) {
218+
if (isError(error, 'TRANSACTION_REPLACED')) {
219+
const reason = error.reason;
220+
if (reason === 'cancelled') {
221+
throw new SignerError(
222+
SignerErrorCode.SEND_TX_ERROR,
223+
undefined,
224+
'Transaction replaced and canceled by user',
225+
undefined,
226+
error
227+
);
228+
}
229+
return { hash: error.replacement.hash, response: error.replacement };
230+
} else if (isError(error, 'CALL_EXCEPTION')) {
231+
const tError = await getTenderlyError(chainId, txHash);
232+
if (!!tError) {
233+
throw new SignerError(
234+
SignerErrorCode.TX_FAILED_IN_BLOCKCHAIN,
235+
'Trannsaction failed in blockchain',
236+
tError,
237+
RangoRPCErrorCode.CALL_EXCEPTION,
238+
error
239+
);
240+
} else {
241+
/**
242+
* In cases where the is no error returen from tenderly, we could ignore
243+
* the error and proceed with check status flow.
244+
*/
245+
return { hash: txHash };
246+
}
247+
}
248+
/**
249+
* Ignore other errors in confirming transaction and proceed with check status flow,
250+
* Some times rpc gives internal error or other type of errors even if the transaction succeeded
251+
*/
252+
return { hash: txHash };
253+
}
254+
}
255+
}

signers/signer-evm/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { DefaultEvmSigner } from './signer.js';
22
export { waitMs, cleanEvmError } from './helper.js';
3+
export { HubEvmSigner } from './hub.js';

signers/signer-solana/src/hub.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { ProxiedNamespace } from '@rango-dev/wallets-core';
2+
import type { SolanaActions } from '@rango-dev/wallets-core/namespaces/solana';
3+
import type { SolanaTransaction } from 'rango-types/mainApi';
4+
5+
import { type GenericSigner, SignerError, SignerErrorCode } from 'rango-types';
6+
7+
import {
8+
generalSolanaTransactionExecutor,
9+
type SolanaWeb3Signer,
10+
} from './index.js';
11+
12+
export class HubSolanaSigner implements GenericSigner<SolanaTransaction> {
13+
private namespace: ProxiedNamespace<SolanaActions>;
14+
15+
constructor(namespace: ProxiedNamespace<SolanaActions>) {
16+
this.namespace = namespace;
17+
}
18+
async signMessage(msg: string): Promise<string> {
19+
return this.namespace.signMessage(msg);
20+
}
21+
22+
async signAndSendTx(tx: SolanaTransaction): Promise<{ hash: string }> {
23+
const DefaultSolanaSigner: SolanaWeb3Signer = async (
24+
solanaWeb3Transaction
25+
) => {
26+
const solanaProvider = this.namespace.getInstance();
27+
28+
if (!solanaProvider) {
29+
throw new SignerError(
30+
SignerErrorCode.SIGN_TX_ERROR,
31+
'Solana instance is not available.'
32+
);
33+
}
34+
35+
if (!solanaProvider.publicKey) {
36+
throw new SignerError(
37+
SignerErrorCode.SIGN_TX_ERROR,
38+
'Please make sure the required account is connected properly.'
39+
);
40+
}
41+
42+
if (tx.from !== solanaProvider.publicKey?.toString()) {
43+
throw new SignerError(
44+
SignerErrorCode.SIGN_TX_ERROR,
45+
`Your connected account doesn't match with the required account. Please ensure that you are connected with the correct account and try again.`
46+
);
47+
}
48+
49+
try {
50+
const signedTransaction = await this.namespace.signTransaction(
51+
solanaWeb3Transaction
52+
);
53+
return signedTransaction.serialize();
54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55+
} catch (e: any) {
56+
const REJECTION_CODE = 4001;
57+
if (e && Object.hasOwn(e, 'code') && e.code === REJECTION_CODE) {
58+
throw new SignerError(SignerErrorCode.REJECTED_BY_USER, undefined, e);
59+
}
60+
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, e);
61+
}
62+
};
63+
const hash = await generalSolanaTransactionExecutor(
64+
tx,
65+
DefaultSolanaSigner
66+
);
67+
return { hash };
68+
}
69+
}

signers/signer-solana/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { DefaultSolanaSigner } from './signer.js';
2+
export { HubSolanaSigner } from './hub.js';
23
export {
34
executeSolanaTransaction,
45
generalSolanaTransactionExecutor,

wallets/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@
6969
"react-dom": "^17.0.0 || ^18.0.0"
7070
},
7171
"dependencies": {
72+
"@solana/web3.js": "^1.91.4",
7273
"caip": "^1.1.1",
74+
"ethers": "^6.13.2",
7375
"immer": "^10.0.4",
7476
"rango-types": "^0.1.85",
7577
"zustand": "^4.5.2"

wallets/core/src/namespaces/evm/actions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { recommended as commonRecommended } from '../common/actions.js';
1515
import { CAIP_NAMESPACE } from './constants.js';
1616
import { getAccounts, switchOrAddNetwork } from './utils.js';
1717

18+
export { sendTransaction } from './actions/sendTransaction.js';
19+
export { signMessage } from './actions/signMessage.js';
20+
export { getTransaction } from './actions/getTransaction.js';
21+
1822
export const recommended = [...commonRecommended];
1923

2024
export function connect(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Context } from '../../../hub/namespaces/mod.js';
2+
import type { FunctionWithContext } from '../../../types/actions.js';
3+
import type { EvmActions, ProviderAPI } from '../types.js';
4+
5+
import { BrowserProvider } from 'ethers';
6+
7+
export function getTransaction(
8+
instance: () => ProviderAPI | undefined
9+
): FunctionWithContext<EvmActions['getTransaction'], Context> {
10+
return async (_context, hash: string) => {
11+
const evmInstance = instance();
12+
if (!evmInstance) {
13+
throw new Error(
14+
'Do your wallet injected correctly and is evm compatible?'
15+
);
16+
}
17+
const provider = new BrowserProvider(evmInstance);
18+
19+
return await provider.getTransaction(hash);
20+
};
21+
}

0 commit comments

Comments
 (0)