Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions signers/signer-evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@rango-dev/wallets-core": "^0.44.0",
"ethers": "^6.13.2",
"rango-types": "^0.1.85"
},
Expand Down
255 changes: 255 additions & 0 deletions signers/signer-evm/src/hub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import type { ProxiedNamespace } from '@rango-dev/wallets-core';
import type { EvmActions } from '@rango-dev/wallets-core/namespaces/evm';
import type { EvmTransaction } from 'rango-types/mainApi';

import {
isError,
type TransactionRequest,
type TransactionResponse,
} from 'ethers';
import { BrowserProvider } from 'ethers';
import {
type GenericSigner,
RPCErrorCode as RangoRPCErrorCode,
SignerError,
SignerErrorCode,
} from 'rango-types';

import { cleanEvmError, getTenderlyError, waitMs } from './helper.js';

const waitWithMempoolCheck = async (
namespace: ProxiedNamespace<EvmActions>,
tx: TransactionResponse,
txHash: string,
confirmations?: number
) => {
const TIMEOUT = 3_000;
let finished = false;
return await Promise.race([
(async () => {
await tx.wait(confirmations);
finished = true;
})(),
(async () => {
while (!finished) {
await waitMs(TIMEOUT);
if (finished) {
return null;
}
try {
const mempoolTx = await namespace.getTransaction(txHash);
if (!mempoolTx) {
return null;
}
} catch (error) {
console.log({ error });
return null;
}
}
return null;
})(),
]);
};

const checkChainIdChanged = async (
namespace: ProxiedNamespace<EvmActions>,
chainId: string
) => {
const evmInstance = namespace.getInstance();
if (!evmInstance) {
return true;
}
const provider = new BrowserProvider(evmInstance);
const signerChainId = (await provider.getNetwork()).chainId;
if (
!signerChainId ||
Number(chainId).toString() !== signerChainId.toString()
) {
return true;
}

return false;
};

export class HubEvmSigner implements GenericSigner<EvmTransaction> {
private namespace: ProxiedNamespace<EvmActions>;

constructor(namespace: ProxiedNamespace<EvmActions>) {
this.namespace = namespace;
}

static buildTx(evmTx: EvmTransaction, disableV2 = false): TransactionRequest {
const TO_STRING_BASE = 16;
let tx: TransactionRequest = {};
/*
* it's better to pass 0x instead of undefined, otherwise some wallets could face issue
* https://github.com/WalletConnect/web3modal/issues/1082#issuecomment-1637793242
*/
tx = {
data: evmTx.data || '0x',
};
if (evmTx.from) {
tx = { ...tx, from: evmTx.from };
}
if (evmTx.to) {
tx = { ...tx, to: evmTx.to };
}
if (evmTx.value) {
tx = { ...tx, value: evmTx.value };
}
if (evmTx.nonce) {
tx = { ...tx, nonce: parseInt(evmTx.nonce) };
}
if (evmTx.gasLimit) {
tx = { ...tx, gasLimit: evmTx.gasLimit };
}
if (!disableV2 && evmTx.maxFeePerGas && evmTx.maxPriorityFeePerGas) {
tx = {
...tx,
maxFeePerGas: evmTx.maxFeePerGas,
maxPriorityFeePerGas: evmTx.maxPriorityFeePerGas,
};
} else if (evmTx.gasPrice) {
tx = {
...tx,
gasPrice: '0x' + parseInt(evmTx.gasPrice).toString(TO_STRING_BASE),
};
}
return tx;
}

async signMessage(msg: string): Promise<string> {
try {
return this.namespace.signMessage(msg);
} catch (error) {
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, error);
}
}

async signAndSendTx(
tx: EvmTransaction,
address: string,
chainId: string | null
): Promise<{ hash: string; response: TransactionResponse }> {
try {
try {
const transaction = HubEvmSigner.buildTx(tx);
const response = await this.namespace.sendTransaction(
transaction,
address,
chainId
);
return { hash: response.hash, response };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// retrying EIP-1559 without v2 related fields
if (
!!error?.message &&
typeof error.message === 'string' &&
error.message.indexOf('EIP-1559') !== -1
) {
console.log('retrying EIP-1559 error without v2 fields ...');
const transaction = HubEvmSigner.buildTx(tx, true);
const response = await this.namespace.sendTransaction(
transaction,
address,
chainId
);
return { hash: response.hash, response };
}
throw error;
}
} catch (error) {
throw cleanEvmError(error);
}
}

async wait(
txHash: string,
chainId?: string,
txResponse?: TransactionResponse,
confirmations?: number
): Promise<{ hash: string; response?: TransactionResponse }> {
try {
/*
* if we have transaction response, use that to wait
* otherwise, try to get tx response from the wallet provider
*/
if (txResponse) {
// if we use waitWithMempoolCheck here, we can't detect replaced tx anymore
await txResponse?.wait(confirmations);
return { hash: txHash };
}

// ignore wait if namespace is not connected yet
if (!this.namespace.state()[0]?.()?.connected) {
return { hash: txHash };
}

// ignore wait if namespace does not support getTransaction
if (!('getTransaction' in this.namespace)) {
return { hash: txHash };
}

/*
* don't proceed if signer chain changed or chain id is not specified
* because if user change the wallet network, we receive null on getTransaction
*/
if (!chainId) {
return { hash: txHash };
}

const hasChainIdChanged = await checkChainIdChanged(
this.namespace,
chainId
);
if (hasChainIdChanged) {
return { hash: txHash };
}

const tx = await this.namespace.getTransaction(txHash);
if (!tx) {
throw Error(`Transaction hash '${txHash}' not found in blockchain.`);
}

await waitWithMempoolCheck(this.namespace, tx, txHash, confirmations);
return { hash: txHash };
} catch (error) {
if (isError(error, 'TRANSACTION_REPLACED')) {
const reason = error.reason;
if (reason === 'cancelled') {
throw new SignerError(
SignerErrorCode.SEND_TX_ERROR,
undefined,
'Transaction replaced and canceled by user',
undefined,
error
);
}
return { hash: error.replacement.hash, response: error.replacement };
} else if (isError(error, 'CALL_EXCEPTION')) {
const tError = await getTenderlyError(chainId, txHash);
if (!!tError) {
throw new SignerError(
SignerErrorCode.TX_FAILED_IN_BLOCKCHAIN,
'Trannsaction failed in blockchain',
tError,
RangoRPCErrorCode.CALL_EXCEPTION,
error
);
} else {
/**
* In cases where the is no error returen from tenderly, we could ignore
* the error and proceed with check status flow.
*/
return { hash: txHash };
}
}
/**
* Ignore other errors in confirming transaction and proceed with check status flow,
* Some times rpc gives internal error or other type of errors even if the transaction succeeded
*/
return { hash: txHash };
}
}
}
1 change: 1 addition & 0 deletions signers/signer-evm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { DefaultEvmSigner } from './signer.js';
export { waitMs, cleanEvmError } from './helper.js';
export { HubEvmSigner } from './hub.js';
1 change: 1 addition & 0 deletions signers/signer-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"lint": "eslint \"**/*.{ts,tsx}\""
},
"dependencies": {
"@rango-dev/wallets-core": "^0.44.0",
"@solana/web3.js": "^1.91.4",
"bs58": "^5.0.0",
"promise-retry": "^2.0.1",
Expand Down
69 changes: 69 additions & 0 deletions signers/signer-solana/src/hub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { ProxiedNamespace } from '@rango-dev/wallets-core';
import type { SolanaActions } from '@rango-dev/wallets-core/namespaces/solana';
import type { SolanaTransaction } from 'rango-types/mainApi';

import { type GenericSigner, SignerError, SignerErrorCode } from 'rango-types';

import {
generalSolanaTransactionExecutor,
type SolanaWeb3Signer,
} from './index.js';

export class HubSolanaSigner implements GenericSigner<SolanaTransaction> {
private namespace: ProxiedNamespace<SolanaActions>;

constructor(namespace: ProxiedNamespace<SolanaActions>) {
this.namespace = namespace;
}
async signMessage(msg: string): Promise<string> {
return this.namespace.signMessage(msg);
}

async signAndSendTx(tx: SolanaTransaction): Promise<{ hash: string }> {
const DefaultSolanaSigner: SolanaWeb3Signer = async (
solanaWeb3Transaction
) => {
const solanaProvider = this.namespace.getInstance();

if (!solanaProvider) {
throw new SignerError(
SignerErrorCode.SIGN_TX_ERROR,
'Solana instance is not available.'
);
}

if (!solanaProvider.publicKey) {
throw new SignerError(
SignerErrorCode.SIGN_TX_ERROR,
'Please make sure the required account is connected properly.'
);
}

if (tx.from !== solanaProvider.publicKey?.toString()) {
throw new SignerError(
SignerErrorCode.SIGN_TX_ERROR,
`Your connected account doesn't match with the required account. Please ensure that you are connected with the correct account and try again.`
);
}

try {
const signedTransaction = await this.namespace.signTransaction(
solanaWeb3Transaction
);
return signedTransaction.serialize();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
const REJECTION_CODE = 4001;
if (e && Object.hasOwn(e, 'code') && e.code === REJECTION_CODE) {
throw new SignerError(SignerErrorCode.REJECTED_BY_USER, undefined, e);
}
throw new SignerError(SignerErrorCode.SIGN_TX_ERROR, undefined, e);
}
};
const hash = await generalSolanaTransactionExecutor(
tx,
DefaultSolanaSigner
);
return { hash };
}
}
1 change: 1 addition & 0 deletions signers/signer-solana/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { DefaultSolanaSigner } from './signer.js';
export { HubSolanaSigner } from './hub.js';
export {
executeSolanaTransaction,
generalSolanaTransactionExecutor,
Expand Down
2 changes: 2 additions & 0 deletions wallets/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@solana/web3.js": "^1.91.4",
"caip": "^1.1.1",
"ethers": "^6.13.2",
"immer": "^10.0.4",
"rango-types": "^0.1.85",
"zustand": "^4.5.2"
Expand Down
4 changes: 4 additions & 0 deletions wallets/core/src/namespaces/evm/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { recommended as commonRecommended } from '../common/actions.js';
import { CAIP_NAMESPACE } from './constants.js';
import { getAccounts, switchOrAddNetwork } from './utils.js';

export { sendTransaction } from './actions/sendTransaction.js';
export { signMessage } from './actions/signMessage.js';
export { getTransaction } from './actions/getTransaction.js';

export const recommended = [...commonRecommended];

export function connect(
Expand Down
21 changes: 21 additions & 0 deletions wallets/core/src/namespaces/evm/actions/getTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Context } from '../../../hub/namespaces/mod.js';
import type { FunctionWithContext } from '../../../types/actions.js';
import type { EvmActions, ProviderAPI } from '../types.js';

import { BrowserProvider } from 'ethers';

export function getTransaction(
instance: () => ProviderAPI | undefined
): FunctionWithContext<EvmActions['getTransaction'], Context> {
return async (_context, hash: string) => {
const evmInstance = instance();
if (!evmInstance) {
throw new Error(
'Do your wallet injected correctly and is evm compatible?'
);
}
const provider = new BrowserProvider(evmInstance);

return await provider.getTransaction(hash);
};
}
Loading