diff --git a/.vscode/settings.json b/.vscode/settings.json index 81e23afff3..59f51dbc0a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": [ "javascript", diff --git a/wallets/provider-all/package.json b/wallets/provider-all/package.json index f8060a5f35..d0ab585679 100644 --- a/wallets/provider-all/package.json +++ b/wallets/provider-all/package.json @@ -42,6 +42,7 @@ "@rango-dev/provider-phantom": "^0.25.1-next.1", "@rango-dev/provider-safe": "^0.18.1-next.1", "@rango-dev/provider-safepal": "^0.25.1-next.1", + "@rango-dev/provider-shapeshift-snap": "^0.25.1-next.1", "@rango-dev/provider-taho": "^0.25.1-next.1", "@rango-dev/provider-tokenpocket": "^0.25.1-next.1", "@rango-dev/provider-tron-link": "^0.25.1-next.1", diff --git a/wallets/provider-all/src/index.ts b/wallets/provider-all/src/index.ts index c154bbf7d0..5995d47783 100644 --- a/wallets/provider-all/src/index.ts +++ b/wallets/provider-all/src/index.ts @@ -19,6 +19,7 @@ import * as okx from '@rango-dev/provider-okx'; import * as phantom from '@rango-dev/provider-phantom'; import * as safe from '@rango-dev/provider-safe'; import * as safepal from '@rango-dev/provider-safepal'; +import * as shapeShiftSnap from '@rango-dev/provider-shapeshift-snap'; import * as taho from '@rango-dev/provider-taho'; import * as tokenpocket from '@rango-dev/provider-tokenpocket'; import * as tronLink from '@rango-dev/provider-tron-link'; @@ -59,5 +60,6 @@ export const allProviders = (enviroments?: Enviroments) => { frontier, taho, braavos, + shapeShiftSnap, ]; }; diff --git a/wallets/provider-shapeshift-snap/CHANGELOG.md b/wallets/provider-shapeshift-snap/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/wallets/provider-shapeshift-snap/package.json b/wallets/provider-shapeshift-snap/package.json new file mode 100644 index 0000000000..39bb56925a --- /dev/null +++ b/wallets/provider-shapeshift-snap/package.json @@ -0,0 +1,31 @@ +{ + "name": "@rango-dev/provider-shapeshift-snap", + "version": "0.25.1-next.1", + "license": "MIT", + "type": "module", + "source": "./src/index.ts", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "typings": "dist/index.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "node ../../scripts/build/command.mjs --path wallets/provider-shapeshift-snap", + "ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json", + "clean": "rimraf dist", + "format": "prettier --write '{.,src}/**/*.{ts,tsx}'", + "lint": "eslint \"**/*.{ts,tsx}\" --ignore-path ../../.eslintignore" + }, + "dependencies": { + "@rango-dev/signer-evm": "^0.25.0", + "@rango-dev/wallets-shared": "^0.25.1-next.1", + "rango-types": "^0.1.57" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/wallets/provider-shapeshift-snap/readme.md b/wallets/provider-shapeshift-snap/readme.md new file mode 100644 index 0000000000..13594f4c25 --- /dev/null +++ b/wallets/provider-shapeshift-snap/readme.md @@ -0,0 +1 @@ +# @rango-dev/provider-shapeshift-snap diff --git a/wallets/provider-shapeshift-snap/src/helpers.ts b/wallets/provider-shapeshift-snap/src/helpers.ts new file mode 100644 index 0000000000..2e57262b43 --- /dev/null +++ b/wallets/provider-shapeshift-snap/src/helpers.ts @@ -0,0 +1,249 @@ +import type { ProviderConnectResult } from '@rango-dev/wallets-shared'; + +import { + getCoinbaseInstance, + Networks, + walletInvokeSnap, + walletRequestSnaps, +} from '@rango-dev/wallets-shared'; + +export const DEFAULT_SNAP_ID = 'npm:@shapeshiftoss/metamask-snaps'; +export const DEFAULT_SNAP_VERSION = '1.0.0'; + +export const SHAPESHIFT_SNAP_SUPPORTED_CHAINS = [ + Networks.BTC, + Networks.BCH, + Networks.COSMOS, + Networks.OSMOSIS, + Networks.DOGE, + Networks.ETHEREUM, + Networks.LTC, + Networks.THORCHAIN, +]; + +const snapNetworksConfig: { + chainId: string; + method: string; + params: { + addressParams: { + coin?: string; + addressNList?: number[]; + scriptType?: string; + }; + }; +}[] = [ + { + chainId: Networks.BTC, + method: 'btc_getAddress', + params: { + addressParams: { + coin: 'Bitcoin', + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 0, 0x80000000 + 0, 0, 0], + scriptType: 'p2pkh', + }, + }, + }, + { + chainId: Networks.BCH, + method: 'bch_getAddress', + params: { + addressParams: { + coin: 'BitcoinCash', + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 145, 0x80000000 + 0, 0, 0], + scriptType: 'p2pkh', + }, + }, + }, + { + chainId: Networks.LTC, + method: 'ltc_getAddress', + params: { + addressParams: { + coin: 'Litecoin', + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 2, 0x80000000 + 0, 0, 0], + scriptType: 'p2pkh', + }, + }, + }, + /* + * { + * chainId: Networks.DOGE, + * method: 'doge_getAddress', + * params: { + * addressParams: { + * coin: 'Dogecoin', + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 3, 0x80000000 + 0, 0, 0], + * scriptType: 'p2pkh', + * }, + * }, + * }, + */ + /* + * { + * chainId: Networks.BINANCE, + * method: 'binance_getAddress', + * params: { + * addressParams: { + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 714, 0x80000000 + 0, 0, 0], + * }, + * }, + * }, + */ + + { + chainId: Networks.COSMOS, + method: 'cosmos_getAddress', + params: { + addressParams: { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 118, 0x80000000 + 0, 0, 0], + }, + }, + }, + /* + * { + * chainId: Networks.ETHEREUM, + * method: 'eth_getAddress', + * params: { + * addressParams: { + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0], + * }, + * }, + * }, + */ + { + chainId: 'osmosis-1', + method: 'osmosis_getAddress', + params: { + addressParams: { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 118, 0x80000000 + 0, 0, 0], + }, + }, + }, + /* + * { + * chainId: Networks.SECRET, + * method: 'secret_getAddress', + * params: { + * addressParams: { + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 529, 0x80000000 + 0, 0, 0], + * }, + * }, + * }, + */ + /* + * { + * chainId: Networks.TERRA, + * method: 'terra_getAddress', + * params: { + * addressParams: { + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 330, 0x80000000 + 0, 0, 0], + * }, + * }, + * }, + */ + { + chainId: Networks.THORCHAIN, + method: 'thorchain_getAddress', + params: { + addressParams: { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [0x80000000 + 44, 0x80000000 + 931, 0x80000000 + 0, 0, 0], + }, + }, + }, + /* + * { + * chainId: Networks.AVAX_CCHAIN, + * method: 'avax_getAddress', + * params: { + * addressParams: { + * // eslint-disable-next-line @typescript-eslint/no-magic-numbers + * addressNList: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0], + * }, + * }, + * }, + */ +]; + +export function metamask() { + const isCoinbaseWalletAvailable = !!getCoinbaseInstance(); + const { ethereum } = window; + + // Some wallets overriding the metamask. So we need to get it properly. + if (isCoinbaseWalletAvailable) { + // Getting intance from overrided structure from coinbase. + return getCoinbaseInstance('metamask'); + } + if (!!ethereum && ethereum.isMetaMask) { + return ethereum; + } + + return null; +} + +export const installSnap = async (instance: any) => { + return walletRequestSnaps({ + instance, + snapId: DEFAULT_SNAP_ID, + version: DEFAULT_SNAP_VERSION, + }); +}; + +export const getAccounts = async ( + instance: any +): Promise => { + const MAX_CONCURRENT_REQUESTS = 4; // Implemented this way to revent following error: "Exceeds maximum number of requests waiting to be resolved" + + const resolvedAccounts: ProviderConnectResult[] = []; + let index = 0; + + while (index < snapNetworksConfig.length) { + const batchConfigs = snapNetworksConfig.slice( + index, + index + MAX_CONCURRENT_REQUESTS + ); + const batchPromises = batchConfigs.map(async (item) => { + let address: string = await walletInvokeSnap({ + instance, + params: { + snapId: DEFAULT_SNAP_ID, + request: { + method: item.method, + params: item.params, + }, + }, + }); + + if (address.includes(':')) { + address = address.split(':')[1]; // this line is here because bitcoin cash address returns like "bitcoincash:..." + } + + return { + accounts: [address], + chainId: item.chainId, + }; + }); + const availableAccountForChains = await Promise.allSettled(batchPromises); + + availableAccountForChains.forEach((result) => { + if (result.status === 'fulfilled') { + const { accounts, chainId } = result.value; + resolvedAccounts.push({ accounts, chainId }); + } + }); + + index += MAX_CONCURRENT_REQUESTS; + } + + return resolvedAccounts; +}; diff --git a/wallets/provider-shapeshift-snap/src/index.ts b/wallets/provider-shapeshift-snap/src/index.ts new file mode 100644 index 0000000000..729bf8ea55 --- /dev/null +++ b/wallets/provider-shapeshift-snap/src/index.ts @@ -0,0 +1,63 @@ +import type { + Connect, + Networks, + ProviderConnectResult, + WalletInfo, +} from '@rango-dev/wallets-shared'; +import type { BlockchainMeta, SignerFactory } from 'rango-types'; + +import { WalletTypes } from '@rango-dev/wallets-shared'; + +import { + getAccounts, + installSnap, + metamask as metamask_instance, + SHAPESHIFT_SNAP_SUPPORTED_CHAINS, +} from './helpers'; +import signer from './signer'; + +const WALLET = WalletTypes.SHAPESHIFT_SNAP; + +export const config = { + type: WALLET, +}; + +export const getInstance = metamask_instance; +export const connect: Connect = async ({ instance }: { instance: any }) => { + let accounts: ProviderConnectResult[] = []; + + const installed = await installSnap(instance); + + if (!!installed) { + accounts = await getAccounts(instance); + } + + if (!accounts?.length) { + throw new Error('Please make sure ShapeShift Snap is installed.'); + } + + return accounts; +}; + +export const getSigners: (provider: any) => SignerFactory = signer; + +export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = ( + allBlockChains +) => { + return { + name: 'ShapeShift Snap', + img: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/metamask/icon.svg', + installLink: { + CHROME: 'https://shapeshift.com/snap', + BRAVE: 'https://shapeshift.com/snap', + + FIREFOX: 'https://shapeshift.com/snap', + EDGE: 'https://shapeshift.com/snap', + DEFAULT: 'https://shapeshift.com/snap', + }, + color: '#dac7ae', + supportedChains: allBlockChains.filter((blockchainMeta) => + SHAPESHIFT_SNAP_SUPPORTED_CHAINS.includes(blockchainMeta.name as Networks) + ), + }; +}; diff --git a/wallets/provider-shapeshift-snap/src/signer.ts b/wallets/provider-shapeshift-snap/src/signer.ts new file mode 100644 index 0000000000..8b0273d17b --- /dev/null +++ b/wallets/provider-shapeshift-snap/src/signer.ts @@ -0,0 +1,19 @@ +import type { SignerFactory } from 'rango-types'; + +import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types'; + +import ShapeShiftSnapBaseSigner from './signer/shapeShiftSnapBaseSigner'; +import ShapeShiftSnapCosmosSigner from './signer/shapeShiftSnapCosmosSigner'; + +export default function getSigners(provider: any): SignerFactory { + const signers = new DefaultSignerFactory(); + signers.registerSigner( + TxType.TRANSFER, + new ShapeShiftSnapBaseSigner(provider) + ); + signers.registerSigner( + TxType.COSMOS, + new ShapeShiftSnapCosmosSigner(provider) + ); + return signers; +} diff --git a/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapBaseSigner.ts b/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapBaseSigner.ts new file mode 100644 index 0000000000..f40b36a859 --- /dev/null +++ b/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapBaseSigner.ts @@ -0,0 +1,98 @@ +import type { GenericSigner, Transfer } from 'rango-types'; + +import { walletInvokeSnap } from '@rango-dev/wallets-shared'; +import { SignerError, SignerErrorCode } from 'rango-types'; + +import { DEFAULT_SNAP_ID } from '../helpers'; + +class ShapeShiftSnapBaseSigner implements GenericSigner { + private provider: any; + + constructor(provider: any) { + this.provider = provider; + } + + public async signMessage(): Promise { + throw SignerError.UnimplementedError('signMessage'); + } + + async signAndSendTx( + tx: Transfer, + address: string + ): Promise<{ hash: string }> { + try { + const temp1 = await fetch( + `https://api.litecoin.shapeshift.com/api/v1/account/${address}/utxos`, + { + method: 'GET', + } + ); + const temp1Json = await temp1.json(); + + const temp2 = await fetch( + `https://api.litecoin.shapeshift.com/api/v1/tx/${temp1Json?.[0]?.txid}`, + { + method: 'GET', + } + ); + const temp2Json = await temp2.json(); + + const signresult = await walletInvokeSnap({ + instance: this.provider, + params: { + snapId: DEFAULT_SNAP_ID, + request: { + method: 'ltc_signTransaction', + params: { + transaction: { + coin: 'Litecoin', + inputs: [ + { + addressNList: temp1Json?.[0]?.path, + scriptType: 'p2pkh', + amount: temp1Json?.[0]?.value, + vout: temp1Json?.[0]?.vout, + txid: temp1Json?.[0]?.txid, + hex: temp2Json?.hex, + }, + ], + outputs: [ + { + addressType: 'spend', + amount: tx.amount, + address: tx.recipientAddress, + }, + { + addressType: 'change', + amount: ( + temp1Json?.[0]?.value - parseInt(tx.amount) + ).toString(), + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + addressNList: [2147483692, 2147483650, 2147483648, 1, 0], + scriptType: 'p2pkh', + isChange: true, + }, + ], + opReturnData: tx.memo, + }, + }, + }, + }, + }); + + console.log(signresult); + + return { hash: '' }; + } catch (err) { + console.log({ err }); + + if (SignerError.isSignerError(err)) { + throw err; + } else { + throw new SignerError(SignerErrorCode.SEND_TX_ERROR, undefined, err); + } + } + } +} + +export default ShapeShiftSnapBaseSigner; diff --git a/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapCosmosSigner.ts b/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapCosmosSigner.ts new file mode 100644 index 0000000000..f8a60516cd --- /dev/null +++ b/wallets/provider-shapeshift-snap/src/signer/shapeShiftSnapCosmosSigner.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers */ +import type { CosmosTransaction, GenericSigner } from 'rango-types'; + +import { walletInvokeSnap } from '@rango-dev/wallets-shared'; +import { SignerError, SignerErrorCode } from 'rango-types'; + +import { DEFAULT_SNAP_ID } from '../helpers'; + +const signCosmosTransaction = async (instance: any, tx: CosmosTransaction) => { + const signedTx = await walletInvokeSnap({ + instance, + params: { + snapId: DEFAULT_SNAP_ID, + request: { + method: 'cosmos_signTransaction', + params: { + transaction: { + addressNList: [ + 0x80000000 + 44, + 0x80000000 + 118, + 0x80000000 + 0, + 0, + 0, + ], + chain_id: tx.data.chainId, + account_number: tx.data.account_number, + sequence: tx.data.sequence, + tx: { + fee: tx.data.fee, + memo: tx.data.memo, + msg: tx.data.msgs, + }, + }, + }, + }, + }, + }); + + return signedTx; +}; + +const broadcastCosmosTransaction = async (instance: any, signedTx: any) => { + const result = await fetch('https://api.cosmos.shapeshift.com/api/v1/send', { + method: 'POST', + body: JSON.stringify({ + rawTx: signedTx.serialized, + }), + }); + + const resultJson = await result.json(); + return resultJson; +}; + +class ShapeShiftSnapCosmosSigner implements GenericSigner { + private provider: any; + + constructor(provider: any) { + this.provider = provider; + } + + public async signMessage(): Promise { + throw SignerError.UnimplementedError('signMessage'); + } + + async signAndSendTx(tx: CosmosTransaction): Promise<{ hash: string }> { + try { + const signedTx = await signCosmosTransaction(this.provider, tx); + const result = await broadcastCosmosTransaction(this.provider, signedTx); + + return { hash: result }; + } catch (err) { + console.log({ err }); + + if (SignerError.isSignerError(err)) { + throw err; + } else { + throw new SignerError(SignerErrorCode.SEND_TX_ERROR, undefined, err); + } + } + } +} + +export default ShapeShiftSnapCosmosSigner; diff --git a/wallets/provider-shapeshift-snap/tsconfig.json b/wallets/provider-shapeshift-snap/tsconfig.json new file mode 100644 index 0000000000..7f49cdfc54 --- /dev/null +++ b/wallets/provider-shapeshift-snap/tsconfig.json @@ -0,0 +1,11 @@ +{ + // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs + "extends": "../../tsconfig.lib.json", + "include": ["src", "types", "../../global-wallets-env.d.ts"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "lib": ["dom", "esnext"] + // match output dir to input dir. e.g. dist/index instead of dist/src/index + } +} diff --git a/wallets/shared/src/index.ts b/wallets/shared/src/index.ts index 68f5fd78be..52a4a6268d 100644 --- a/wallets/shared/src/index.ts +++ b/wallets/shared/src/index.ts @@ -2,3 +2,4 @@ export * from './helpers'; export * from './providers'; export * from './rango'; export * from './getCosmosAccounts'; +export * from './snaps'; diff --git a/wallets/shared/src/rango.ts b/wallets/shared/src/rango.ts index 2367cc779a..99682bc346 100644 --- a/wallets/shared/src/rango.ts +++ b/wallets/shared/src/rango.ts @@ -79,6 +79,7 @@ export enum WalletTypes { ENKRYPT = 'enkrypt', TAHO = 'taho', MY_TON_WALLET = 'mytonwallet', + SHAPESHIFT_SNAP = 'shapeShiftSnap', } export enum Networks { diff --git a/wallets/shared/src/snaps.ts b/wallets/shared/src/snaps.ts new file mode 100644 index 0000000000..984249e827 --- /dev/null +++ b/wallets/shared/src/snaps.ts @@ -0,0 +1,54 @@ +export const walletRequestSnaps = async ({ + instance, + snapId, + version, +}: { + instance: any; + snapId: string; + version?: string; +}): Promise => { + if (instance === undefined) { + throw new Error('Could not get MetaMask provider'); + } + if (instance.request === undefined) { + throw new Error('MetaMask provider does not define a .request() method'); + } + + try { + const ret = await instance.request({ + method: 'wallet_requestSnaps', + params: { + [snapId]: { version }, + }, + }); + return ret; + } catch (error) { + console.log('wallet_requestSnaps RPC call failed.', error); + return Promise.reject(error); + } +}; + +export const walletInvokeSnap = async ({ + instance, + params, +}: { + instance: any; + params?: any; +}): Promise => { + if (instance === undefined) { + throw new Error('Could not get MetaMask provider'); + } + if (instance.request === undefined) { + throw new Error('MetaMask provider does not define a .request() method'); + } + try { + const ret = await instance.request({ + method: 'wallet_invokeSnap', + params, + }); + return ret; + } catch (error) { + console.log('wallet_invokeSnap RPC call failed.', error); + return Promise.reject(error); + } +};