diff --git a/queue-manager/rango-preset/src/actions/scheduleNextStep.ts b/queue-manager/rango-preset/src/actions/scheduleNextStep.ts index f08b6057be..e285b26434 100644 --- a/queue-manager/rango-preset/src/actions/scheduleNextStep.ts +++ b/queue-manager/rango-preset/src/actions/scheduleNextStep.ts @@ -36,6 +36,8 @@ export function scheduleNextStep({ (step: PendingSwapStep) => step.status === 'failed' ); + console.log('what the heck is happening here'); + if (!!currentStep && !isFailed) { if (isTxAlreadyCreated(swap, currentStep)) { if (currentStep.fromBlockchain === TransactionType.XRPL) { diff --git a/wallets/core/src/namespaces/xrpl/mod.ts b/wallets/core/src/namespaces/xrpl/mod.ts index c8c90bb838..62581966b3 100644 --- a/wallets/core/src/namespaces/xrpl/mod.ts +++ b/wallets/core/src/namespaces/xrpl/mod.ts @@ -1,4 +1,5 @@ export * as builders from './builders.js'; export type { XRPLActions } from './types.js'; +export * as utils from './utils.js'; export { CAIP_XRPL_CHAIN_ID, CAIP_NAMESPACE } from './constants.js'; diff --git a/wallets/core/src/namespaces/xrpl/utils.ts b/wallets/core/src/namespaces/xrpl/utils.ts new file mode 100644 index 0000000000..581fc48faf --- /dev/null +++ b/wallets/core/src/namespaces/xrpl/utils.ts @@ -0,0 +1,15 @@ +import type { CaipAccount } from '../common/mod.js'; + +import { AccountId } from 'caip'; + +import { CAIP_NAMESPACE, CAIP_XRPL_CHAIN_ID } from './constants.js'; + +export function formatAddressToCAIP(address: string): string { + return AccountId.format({ + address, + chainId: { + namespace: CAIP_NAMESPACE, + reference: CAIP_XRPL_CHAIN_ID, + }, + }) as CaipAccount; +} diff --git a/wallets/provider-all/package.json b/wallets/provider-all/package.json index ba8fb0a152..84e3a4e4ae 100644 --- a/wallets/provider-all/package.json +++ b/wallets/provider-all/package.json @@ -32,6 +32,7 @@ "@rango-dev/provider-enkrypt": "^0.58.1-next.2", "@rango-dev/provider-exodus": "^0.59.1-next.2", "@rango-dev/provider-keplr": "^0.58.1-next.2", + "@rango-dev/provider-gemwallet": "^0.1.0", "@rango-dev/provider-leap-cosmos": "^0.58.1-next.2", "@rango-dev/provider-ledger": "^0.29.1-next.2", "@rango-dev/provider-math-wallet": "^0.59.1-next.2", @@ -62,4 +63,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/wallets/provider-all/src/index.ts b/wallets/provider-all/src/index.ts index 56a9da02df..d4e5721363 100644 --- a/wallets/provider-all/src/index.ts +++ b/wallets/provider-all/src/index.ts @@ -14,6 +14,7 @@ import { versions as cosmostation } from '@rango-dev/provider-cosmostation'; import * as defaultInjected from '@rango-dev/provider-default'; import { versions as enkrypt } from '@rango-dev/provider-enkrypt'; import { versions as exodus } from '@rango-dev/provider-exodus'; +import { versions as gemwallet } from '@rango-dev/provider-gemwallet'; import { versions as keplr } from '@rango-dev/provider-keplr'; import { versions as leap } from '@rango-dev/provider-leap-cosmos'; import { versions as ledger } from '@rango-dev/provider-ledger'; @@ -130,5 +131,6 @@ export const allProviders = ( solflare, slush, unisat, + gemwallet, ]; }; diff --git a/wallets/provider-gemwallet/package.json b/wallets/provider-gemwallet/package.json new file mode 100644 index 0000000000..b92a3f054d --- /dev/null +++ b/wallets/provider-gemwallet/package.json @@ -0,0 +1,32 @@ +{ + "name": "@rango-dev/provider-gemwallet", + "version": "0.1.0", + "license": "MIT", + "type": "module", + "source": "./src/mod.ts", + "main": "./dist/mod.js", + "exports": { + ".": "./dist/mod.js" + }, + "typings": "dist/mod.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "node ../../scripts/build/command.mjs --path wallets/provider-gemwallet --inputs src/mod.ts", + "ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json", + "clean": "rimraf dist", + "format": "prettier --write '{.,src}/**/*.{ts,tsx}'", + "lint": "eslint \"**/*.{ts,tsx}\"" + }, + "dependencies": { + "@rango-dev/wallets-shared": "0.58.1-next.2", + "@gemwallet/api": "^3.8.0", + "xrpl": "^4.2.0", + "rango-types": "^0.1.95" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/wallets/provider-gemwallet/readme.md b/wallets/provider-gemwallet/readme.md new file mode 100644 index 0000000000..d0d1755f8c --- /dev/null +++ b/wallets/provider-gemwallet/readme.md @@ -0,0 +1,18 @@ +# GemWallet Provider +GemWallet integration for hub. +[Homepage](https://gemwallet.app/) | [Docs](https://gemwallet.app/docs/user-guide/introduction) + +More about implementation status can be found [here](../readme.md). + +## Implementation notes/limitation + +### Feature + +#### ⚠️ Auto Connect + +It doesn't have the feature to silently connect to wallet, it shows a popup and a loading for a few seconds. + + +--- + +More wallet information can be found in [readme.md](../readme.md). diff --git a/wallets/provider-gemwallet/src/constants.ts b/wallets/provider-gemwallet/src/constants.ts new file mode 100644 index 0000000000..c169469edc --- /dev/null +++ b/wallets/provider-gemwallet/src/constants.ts @@ -0,0 +1,40 @@ +import type { ProviderMetadata } from '@rango-dev/wallets-core'; +import type { BlockchainMeta } from 'rango-types'; + +import { xrplBlockchain } from 'rango-types'; + +import getSigners from './signer.js'; + +export const XRPL_PUBLIC_SERVER = 'wss://xrplcluster.com/'; +export const WALLET_ID = 'gemwallet'; + +export const info: ProviderMetadata = { + name: 'GemWallet', + icon: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/gemwallet/icon.svg', + extensions: { + chrome: + 'https://chromewebstore.google.com/detail/gemwallet/egebedonbdapoieedfcfkofloclfghab', + homepage: 'https://gemwallet.app/', + }, + properties: [ + { + name: 'namespaces', + value: { + selection: 'multiple', + data: [ + { + label: 'XRPL', + value: 'XRPL', + id: 'XRPL', + getSupportedChains: (allBlockchains: BlockchainMeta[]) => + xrplBlockchain(allBlockchains), + }, + ], + }, + }, + { + name: 'signers', + value: { getSigners: async () => getSigners() }, + }, + ], +}; diff --git a/wallets/provider-gemwallet/src/mod.ts b/wallets/provider-gemwallet/src/mod.ts new file mode 100644 index 0000000000..3ac86548e6 --- /dev/null +++ b/wallets/provider-gemwallet/src/mod.ts @@ -0,0 +1,8 @@ +import { defineVersions } from '@rango-dev/wallets-core/utils'; + +import { buildProvider } from './provider.js'; + +const versions = () => + defineVersions().version('1.0.0', buildProvider()).build(); + +export { versions }; diff --git a/wallets/provider-gemwallet/src/namespaces/xrpl/helpers.ts b/wallets/provider-gemwallet/src/namespaces/xrpl/helpers.ts new file mode 100644 index 0000000000..02f0722233 --- /dev/null +++ b/wallets/provider-gemwallet/src/namespaces/xrpl/helpers.ts @@ -0,0 +1,88 @@ +import type { + Memo, + SendPaymentRequest, + SetTrustlineRequest, +} from '@gemwallet/api'; +import type { + XrplPaymentTransactionData, + XrplTransactionDataIssuedCurrencyAmount, + XrplTransactionDataMPTAmount, + XrplTrustSetTransactionData, +} from 'rango-types/mainApi'; + +function isIssuedCurrencyAmount( + amount: XrplPaymentTransactionData['Amount'] +): amount is XrplTransactionDataIssuedCurrencyAmount { + return ( + typeof amount === 'object' && + // @ts-expect-error it never throw an runtime error, since we are checking it should be an object first + typeof amount.currency === 'string' && + // @ts-expect-error it never throw an runtime error, since we are checking it should be an object first + typeof amount.issuer === 'string' && + typeof amount.value === 'string' + ); +} + +function isMPTokenAmount( + amount: XrplPaymentTransactionData['Amount'] +): amount is XrplTransactionDataMPTAmount { + return ( + typeof amount === 'object' && + // @ts-expect-error it never throw an runtime error, since we are checking it should be an object first + typeof amount.mpt_issuance_id === 'string' && + typeof amount.value === 'string' + ); +} + +function fromPaymentTransactionMemoToGemWalletMemo( + memos: XrplPaymentTransactionData['Memos'] +): Memo[] { + if (!memos) { + return []; + } + + return memos.map((memo) => { + return { + memo: { + memoType: memo.Memo.MemoType, + memoData: memo.Memo.MemoData, + memoFormat: memo.Memo.MemoFormat, + }, + }; + }); +} + +export function fromTrustSetTransactionDataToGemWalletRequest( + data: XrplTrustSetTransactionData +): SetTrustlineRequest { + return { + limitAmount: data.LimitAmount, + memos: fromPaymentTransactionMemoToGemWalletMemo(data.Memos), + flags: data.Flags, + }; +} + +export function fromPaymentTransactionDataToGemWalletRequest( + data: XrplPaymentTransactionData +): SendPaymentRequest { + let amount: SendPaymentRequest['amount']; + if (isMPTokenAmount(data.Amount)) { + throw new Error("Current implemented signer doesn't have support for MPT"); + } else if (isIssuedCurrencyAmount(data.Amount)) { + amount = data.Amount; + } else if (typeof data.Amount === 'string') { + amount = data.Amount; + } else { + throw new Error( + "There is an unexpected type for Amount. current signer doesn't have support for that." + ); + } + + return { + amount, + destination: data.Destination, + destinationTag: data.DestinationTag, + memos: fromPaymentTransactionMemoToGemWalletMemo(data.Memos), + flags: data.Flags, + }; +} diff --git a/wallets/provider-gemwallet/src/namespaces/xrpl/hooks.ts b/wallets/provider-gemwallet/src/namespaces/xrpl/hooks.ts new file mode 100644 index 0000000000..ba7cd95251 --- /dev/null +++ b/wallets/provider-gemwallet/src/namespaces/xrpl/hooks.ts @@ -0,0 +1,35 @@ +import type { XRPLActions } from '@rango-dev/wallets-core/namespaces/xrpl'; + +import { on } from '@gemwallet/api'; +import { ChangeAccountSubscriberBuilder } from '@rango-dev/wallets-core/namespaces/common'; +import { utils } from '@rango-dev/wallets-core/namespaces/xrpl'; + +type WalletChangedEventPayload = { + wallet: { + publicAddress: string; + }; +}; + +export function changeAccountSubscriberBuilder() { + // `true` instead of ProviderAPI is just a workaround. we don't need to have instance here. + return new ChangeAccountSubscriberBuilder< + WalletChangedEventPayload, + true, + XRPLActions + >() + .getInstance(() => true) + .format(async (_, payload) => [ + utils.formatAddressToCAIP(payload.wallet.publicAddress), + ]) + .addEventListener((_, callback) => { + on('walletChanged', callback); + }) + .removeEventListener((_instance, _callback) => { + /* + * TODO: gem wallet doesn't have support for unsubscribing. + * Making a variable and keep the callback refrence then here make it `undefined` is a quick fix + * but it makes new bugs, where if two subscribers added at once, we will loose the track of the first one and it will be staled. + */ + }) + .build(); +} diff --git a/wallets/provider-gemwallet/src/namespaces/xrpl/mod.ts b/wallets/provider-gemwallet/src/namespaces/xrpl/mod.ts new file mode 100644 index 0000000000..fe6b180e31 --- /dev/null +++ b/wallets/provider-gemwallet/src/namespaces/xrpl/mod.ts @@ -0,0 +1,2 @@ +export { namespace } from './namespace.js'; +export { Signer } from './singer.js'; diff --git a/wallets/provider-gemwallet/src/namespaces/xrpl/namespace.ts b/wallets/provider-gemwallet/src/namespaces/xrpl/namespace.ts new file mode 100644 index 0000000000..19198d4cf7 --- /dev/null +++ b/wallets/provider-gemwallet/src/namespaces/xrpl/namespace.ts @@ -0,0 +1,75 @@ +import type { XRPLActions } from '@rango-dev/wallets-core/namespaces/xrpl'; + +import { getAddress } from '@gemwallet/api'; +import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core'; +import { builders, utils } from '@rango-dev/wallets-core/namespaces/xrpl'; +import { Client } from 'xrpl'; + +import { WALLET_ID, XRPL_PUBLIC_SERVER } from '../../constants.js'; +import { checkInstallationOnLoad } from '../../utils.js'; + +import { changeAccountSubscriberBuilder } from './hooks.js'; + +const [changeAccountSubscriber, changeAccountCleanup] = + changeAccountSubscriberBuilder(); + +const connect = builders + .connect() + .action(async function () { + const response = await getAddress(); + if (!response.result?.address) { + throw new Error(`Couldn't access to your wallet address.`); + } + + return [utils.formatAddressToCAIP(response.result.address)]; + }) + .before(changeAccountSubscriber) + .or(changeAccountCleanup) + .build(); + +const canEagerConnect = new ActionBuilder( + 'canEagerConnect' +) + .action(async () => { + const isInstalled = await checkInstallationOnLoad(); + if (!isInstalled) { + throw new Error( + 'Trying to eagerly connect to your EVM wallet, but seems its instance is not available.' + ); + } + + try { + const response = await getAddress(); + const address = response.result?.address; + + return !!address; + } catch { + return false; + } + }) + .build(); + +const accountLines = new ActionBuilder( + 'accountLines' +) + .action(async (_, account, options) => { + const client = new Client(XRPL_PUBLIC_SERVER); + await client.connect(); + + const response = await client.request({ + command: 'account_lines', + ledger_index: 'current', + account: account, + peer: options?.peer, + }); + + await client.disconnect(); + return response.result.lines; + }) + .build(); + +export const namespace = new NamespaceBuilder('XRPL', WALLET_ID) + .action(connect) + .action(canEagerConnect) + .action(accountLines) + .build(); diff --git a/wallets/provider-gemwallet/src/namespaces/xrpl/singer.ts b/wallets/provider-gemwallet/src/namespaces/xrpl/singer.ts new file mode 100644 index 0000000000..d13708c47b --- /dev/null +++ b/wallets/provider-gemwallet/src/namespaces/xrpl/singer.ts @@ -0,0 +1,61 @@ +import type { XrplTransaction } from 'rango-types/mainApi'; + +import { sendPayment, setTrustline } from '@gemwallet/api'; +import { type GenericSigner, SignerError } from 'rango-types'; + +import { + fromPaymentTransactionDataToGemWalletRequest, + fromTrustSetTransactionDataToGemWalletRequest, +} from './helpers.js'; + +export class Signer implements GenericSigner { + async signMessage(): Promise { + throw SignerError.UnimplementedError('signMessage'); + } + + async signAndSendTx(tx: XrplTransaction): Promise<{ hash: string }> { + if (tx.data.TransactionType === 'TrustSet') { + const result = await setTrustline( + fromTrustSetTransactionDataToGemWalletRequest(tx.data) + ); + + if (result.type === 'reject') { + throw new Error('The request has been rejected', { + cause: result, + }); + } + + if (!result.result) { + throw new Error( + 'Unexpected error where the result is not returned. (type: UnreachableCode)' + ); + } + + return { + hash: result.result.hash, + }; + } else if (tx.data.TransactionType === 'Payment') { + const result = await sendPayment( + fromPaymentTransactionDataToGemWalletRequest(tx.data) + ); + + if (result.type === 'reject') { + throw new Error('The request has been rejected', { + cause: result, + }); + } + + if (!result.result) { + throw new Error( + 'Unexpected error where the result is not returned. (type: UnreachableCode)' + ); + } + + return { + hash: result.result.hash, + }; + } + + throw new Error('Unsupported transaction type'); + } +} diff --git a/wallets/provider-gemwallet/src/provider.ts b/wallets/provider-gemwallet/src/provider.ts new file mode 100644 index 0000000000..6d53d13077 --- /dev/null +++ b/wallets/provider-gemwallet/src/provider.ts @@ -0,0 +1,20 @@ +import { ProviderBuilder } from '@rango-dev/wallets-core'; + +import { info, WALLET_ID } from './constants.js'; +import { namespace as xrpl } from './namespaces/xrpl/mod.js'; +import { checkInstallationOnLoad } from './utils.js'; + +const buildProvider = () => + new ProviderBuilder(WALLET_ID) + .init(function (context) { + const [, setState] = context.state(); + const setInstallState = (result: boolean) => + setState('installed', result); + + checkInstallationOnLoad().then(setInstallState).catch(console.error); + }) + .config('metadata', info) + .add('xrpl', xrpl) + .build(); + +export { buildProvider }; diff --git a/wallets/provider-gemwallet/src/signer.ts b/wallets/provider-gemwallet/src/signer.ts new file mode 100644 index 0000000000..9a4c76ac0a --- /dev/null +++ b/wallets/provider-gemwallet/src/signer.ts @@ -0,0 +1,10 @@ +import type { SignerFactory } from 'rango-types'; + +import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types'; + +export default async function getSigners(): Promise { + const { Signer: XrplSigner } = await import('./namespaces/xrpl/mod.js'); + const signers = new DefaultSignerFactory(); + signers.registerSigner(TxType.XRPL, new XrplSigner()); + return signers; +} diff --git a/wallets/provider-gemwallet/src/utils.ts b/wallets/provider-gemwallet/src/utils.ts new file mode 100644 index 0000000000..0dac2fc96e --- /dev/null +++ b/wallets/provider-gemwallet/src/utils.ts @@ -0,0 +1,17 @@ +import { isInstalled } from '@gemwallet/api'; + +export async function checkInstallationOnLoad(): Promise { + return new Promise((resolve, reject) => { + window.addEventListener('load', () => { + isInstalled() + .then((response) => { + if (response.result.isInstalled) { + resolve(true); + } else { + resolve(false); + } + }) + .catch(reject); + }); + }); +} diff --git a/wallets/provider-gemwallet/tsconfig.build.json b/wallets/provider-gemwallet/tsconfig.build.json new file mode 100644 index 0000000000..fc43a1c995 --- /dev/null +++ b/wallets/provider-gemwallet/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs + "extends": "../../tsconfig.libnext.json", + "include": ["src", "types"], + "files": ["../../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/provider-gemwallet/tsconfig.json b/wallets/provider-gemwallet/tsconfig.json new file mode 100644 index 0000000000..a3a0b0f59d --- /dev/null +++ b/wallets/provider-gemwallet/tsconfig.json @@ -0,0 +1 @@ +{ "extends": "./tsconfig.build.json", "include": ["src", "tests"] } diff --git a/wallets/readme.md b/wallets/readme.md index 236e77faf6..92b25be0fb 100644 --- a/wallets/readme.md +++ b/wallets/readme.md @@ -162,6 +162,7 @@ For better user experience, wallet provider tries to connect to a wallet only wh | [Xverse](provider-xverse/readme.md) | ❌ | ⚠️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Tomo](provider-tomo/readme.md) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Coin98](provider-coin98/readme.md) | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| [GemWallet](provider-gemwallet/readme.md) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ## By Feature @@ -194,6 +195,7 @@ For better user experience, wallet provider tries to connect to a wallet only wh | Xverse | ⚠️ | 🚧 | ✅ | Injected | ❌ | | Tomo | ✅ | ✅ | ✅ | Injected | ❌ | | Coin98 | ✅ | ✅ | ❌ | Injected | ❌ | +| GemWallet | ✅ | ❌ | ⚠️ | Injected | ❌ | # Supported Wallets (Legacy) diff --git a/yarn.lock b/yarn.lock index 5e5cfd1dbd..511287abb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4010,6 +4010,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@gemwallet/api@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@gemwallet/api/-/api-3.8.0.tgz#46bc47789848c7ac9cc620613e0a1757dc8668a1" + integrity sha512-hZ6XC0mVm3Q54cgonrzk6tHS/wUMjtPHyqsqbtlnNGPouCR7OIfEDo5Y802qLZ5ah6PskhsK0DouVnwUykEM8Q== + "@gql.tada/cli-utils@1.6.3": version "1.6.3" resolved "https://registry.yarnpkg.com/@gql.tada/cli-utils/-/cli-utils-1.6.3.tgz#b893cec74908da4df0602691e2e0b1497fda8cda"