diff --git a/.env.example b/.env.example index be83464..64ece15 100644 --- a/.env.example +++ b/.env.example @@ -94,6 +94,15 @@ BTC_RECEIVER_ADDRESS='tb1q9uxj8p043sjkm0qzlsys7677mv98j76k8cvgtg' BTC_TRANSFER_AMOUNT=0.00001 #Bitcoin +#XRPl +XRP_TRANSFER_TEST_IS_ACTIVE=false +XRP_LISTENER_TEST_IS_ACTIVE=false +XRP_SENDER_PRIVATE_KEY='shV4vVLq2QZx8zopPSSp9BwaHKGan' +XRP_SENDER_ADDRESS='rMHC2kEJBYTmB8Nk37TgqV8gdxGeexpLe9' +XRP_RECEIVER_ADDRESS='r92HmWXA7dWQ6XNMMJJsbdWJfqYwLVNmGr' +XRP_TRANSFER_AMOUNT=0.001 +#XRPl + #Solana # Assets SOL_COIN_TRANSFER_TEST_IS_ACTIVE=false diff --git a/packages/networks/bitcoin/package.json b/packages/networks/bitcoin/package.json index 269743e..f62d3b8 100644 --- a/packages/networks/bitcoin/package.json +++ b/packages/networks/bitcoin/package.json @@ -63,7 +63,7 @@ ], "author": "MultipleChain", "license": "MIT", - "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/network-name", + "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/bitcoin", "repository": { "type": "git", "url": "git+https://github.com/MultipleChain/js.git" diff --git a/packages/networks/bitcoin/src/services/TransactionListener.ts b/packages/networks/bitcoin/src/services/TransactionListener.ts index 7c4af08..8d8ca48 100644 --- a/packages/networks/bitcoin/src/services/TransactionListener.ts +++ b/packages/networks/bitcoin/src/services/TransactionListener.ts @@ -245,7 +245,7 @@ export class TransactionListener< if ( this.filter?.signer !== undefined && - values.sender !== this.filter.signer.toLowerCase() + values.sender?.toLowerCase() !== this.filter.signer.toLowerCase() ) { return } diff --git a/packages/networks/boilerplate/package.json b/packages/networks/boilerplate/package.json index cbc5909..fa2557f 100644 --- a/packages/networks/boilerplate/package.json +++ b/packages/networks/boilerplate/package.json @@ -63,7 +63,7 @@ ], "author": "MultipleChain", "license": "MIT", - "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/network-name", + "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/boilerplate", "repository": { "type": "git", "url": "git+https://github.com/MultipleChain/js.git" diff --git a/packages/networks/solana/package.json b/packages/networks/solana/package.json index c295a90..ef41398 100644 --- a/packages/networks/solana/package.json +++ b/packages/networks/solana/package.json @@ -1,6 +1,6 @@ { "name": "@multiplechain/solana", - "version": "0.4.15", + "version": "0.4.16", "type": "module", "main": "dist/index.cjs", "module": "dist/index.es.js", @@ -63,7 +63,7 @@ ], "author": "MultipleChain", "license": "MIT", - "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/network-name", + "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/solana", "repository": { "type": "git", "url": "git+https://github.com/MultipleChain/js.git" diff --git a/packages/networks/solana/src/assets/Token.ts b/packages/networks/solana/src/assets/Token.ts index 5cb5b83..24c5fc1 100644 --- a/packages/networks/solana/src/assets/Token.ts +++ b/packages/networks/solana/src/assets/Token.ts @@ -57,23 +57,27 @@ export class Token extends Contract implements TokenInterface 'confirmed', programId ) - if (result === null) return null - return (this.metadata = { - name: result.name, - symbol: result.symbol, - programId: programId.toBase58(), - decimals: accountInfo.value.data.parsed.info.decimals - }) - } else { - const metaplex = Metaplex.make(this.provider.web3) - const data = await metaplex.nfts().findByMint({ mintAddress: this.pubKey }) - return (this.metadata = { - name: data.name, - symbol: data.symbol, - programId: programId.toBase58(), - decimals: accountInfo.value.data.parsed.info.decimals - }) + if (result !== null) { + return (this.metadata = { + name: result.name, + symbol: result.symbol, + programId: programId.toBase58(), + decimals: accountInfo.value.data.parsed.info.decimals + }) + } } + + const metaplex = Metaplex.make(this.provider.web3) + const data = await metaplex.nfts().findByMint({ mintAddress: this.pubKey }) + + if (data === null) return null + + return (this.metadata = { + name: data.name, + symbol: data.symbol, + programId: programId.toBase58(), + decimals: accountInfo.value.data.parsed.info.decimals + }) } /** diff --git a/packages/networks/ton/package.json b/packages/networks/ton/package.json index fb8843e..75813db 100644 --- a/packages/networks/ton/package.json +++ b/packages/networks/ton/package.json @@ -1,6 +1,6 @@ { "name": "@multiplechain/ton", - "version": "0.1.0", + "version": "0.1.10", "type": "module", "main": "dist/index.cjs", "module": "dist/index.es.js", @@ -63,7 +63,7 @@ ], "author": "MultipleChain", "license": "MIT", - "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/network-name", + "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/ton", "repository": { "type": "git", "url": "git+https://github.com/MultipleChain/js.git" diff --git a/packages/networks/ton/src/browser/Wallet.ts b/packages/networks/ton/src/browser/Wallet.ts index 6c89817..e921b54 100644 --- a/packages/networks/ton/src/browser/Wallet.ts +++ b/packages/networks/ton/src/browser/Wallet.ts @@ -2,22 +2,26 @@ import { Provider } from '../services/Provider' import { CHAIN, type TonConnectUI } from '@tonconnect/ui' import type { TransactionSigner } from '../services/TransactionSigner' import { Address, Cell, type CommonMessageInfoRelaxedInternal } from '@ton/core' -import type { - WalletInterface, - WalletAdapterInterface, - WalletPlatformEnum, - TransactionId, - SignedMessage, - WalletAddress, - ConnectConfig, - UnknownConfig +import { + type WalletInterface, + type WalletAdapterInterface, + type WalletPlatformEnum, + type TransactionId, + type SignedMessage, + type WalletAddress, + type ConnectConfig, + type UnknownConfig, + ErrorTypeEnum } from '@multiplechain/types' type WalletAdapter = WalletAdapterInterface const rejectMap = (error: any, reject: (a: any) => any): any => { console.error('MultipleChain TON Connect Error:', error) - // const errorMessage = String(error.message ?? '') + const errorMessage = String(error.message ?? '') + if (errorMessage.includes('Reject request')) { + return reject(ErrorTypeEnum.WALLET_REQUEST_REJECTED) + } return reject(error) } @@ -107,7 +111,14 @@ export class Wallet implements WalletInterface { - rejectMap(error, reject) + const customReject = (error: any): void => { + if (error.message === ErrorTypeEnum.WALLET_REQUEST_REJECTED) { + reject(new Error(ErrorTypeEnum.WALLET_CONNECT_REJECTED)) + } else { + reject(error) + } + } + rejectMap(error, customReject) }) }) } @@ -163,28 +174,42 @@ export class Wallet implements WalletInterface { - const account = this.walletProvider.account - const data = transactionSigner.getRawData() - const info = data.info as CommonMessageInfoRelaxedInternal - const result = await this.walletProvider.sendTransaction( - { - validUntil: Math.floor(Date.now() / 1000) + 60, - from: this.getRawAddress(), - network: account?.chain, - messages: [ - { + return await new Promise((resolve, reject) => { + try { + const account = this.walletProvider.account + const messages = transactionSigner.getRawData() + + const tonConnectMessageFormat = [] + for (const message of messages) { + const info = message.info as CommonMessageInfoRelaxedInternal + tonConnectMessageFormat.push({ address: info.dest.toString(), amount: info.value.coins.toString(), - payload: data.body.toBoc().toString('base64') - } - ] - }, - modalAction - ) - - const messageHash = Cell.fromBase64(result.boc).hash().toString('hex') - - return await this.networkProvider.findTxHashByMessageHash(messageHash) + payload: message.body.toBoc().toString('base64') + }) + } + + this.walletProvider + .sendTransaction( + { + validUntil: Math.floor(Date.now() / 1000) + 60, + from: this.getRawAddress(), + network: account?.chain, + messages: tonConnectMessageFormat + }, + modalAction + ) + .then(async (result) => { + const messageHash = Cell.fromBase64(result.boc).hash().toString('hex') + resolve(await this.networkProvider.findTxHashByMessageHash(messageHash)) + }) + .catch((error) => { + rejectMap(error, reject) + }) + } catch (error) { + rejectMap(error, reject) + } + }) } /** diff --git a/packages/networks/ton/src/browser/adapters/TonConnect.ts b/packages/networks/ton/src/browser/adapters/TonConnect.ts index 9a8e386..26a1f3c 100644 --- a/packages/networks/ton/src/browser/adapters/TonConnect.ts +++ b/packages/networks/ton/src/browser/adapters/TonConnect.ts @@ -7,10 +7,11 @@ import type { ConnectConfig, WalletAdapterInterface } from '@multiplechain/types export type TonConnectConfig = ConnectConfig & { manifestUrl?: string buttonRootId?: string - themeMode?: THEME + themeMode?: string } let ui: TonConnectUI +let rejectedAction: (reason?: any) => void let connectedAction: (value: TonConnectUI | PromiseLike) => void const createUI = (config?: TonConnectConfig): TonConnectUI => { @@ -20,7 +21,7 @@ const createUI = (config?: TonConnectConfig): TonConnectUI => { ui = new TonConnectUI({ uiPreferences: { - theme: config?.themeMode ?? THEME.LIGHT + theme: config?.themeMode === 'light' ? THEME.LIGHT : THEME.DARK }, manifestUrl: config?.manifestUrl, buttonRootId: config?.buttonRootId @@ -33,6 +34,12 @@ const createUI = (config?: TonConnectConfig): TonConnectUI => { } }) + ui.onModalStateChange((state) => { + if (state.status === 'closed' && state.closeReason === 'action-cancelled') { + rejectedAction(ErrorTypeEnum.CLOSED_WALLETCONNECT_MODAL) + } + }) + return ui } @@ -46,8 +53,12 @@ const TonConnect: WalletAdapterInterface = { return ui?.connected ?? false }, disconnect: async () => { - if (ui) { - await ui.disconnect() + try { + if (ui) { + await ui.disconnect() + } + } catch (error) { + console.error(error) } }, connect: async (provider?: Provider, _config?: ConnectConfig) => { @@ -67,6 +78,7 @@ const TonConnect: WalletAdapterInterface = { return await new Promise((resolve, reject) => { try { connectedAction = resolve + rejectedAction = reject void createUI(config).openModal() } catch (error) { reject(error) diff --git a/packages/networks/ton/src/services/Provider.ts b/packages/networks/ton/src/services/Provider.ts index a5cdc2b..e7fb190 100644 --- a/packages/networks/ton/src/services/Provider.ts +++ b/packages/networks/ton/src/services/Provider.ts @@ -298,6 +298,15 @@ export class Provider implements ProviderInterface { }) } + /** + * Create wallet contract for version 4 + * @param publicKey - Public key of the wallet + * @returns Wallet contract + */ + createWalletV4(publicKey: Buffer): WalletContractV4 { + return WalletContractV4.create({ workchain: this.workchain, publicKey }) + } + /** * Retry the function * @param fn - Function that will be retried diff --git a/packages/networks/ton/src/services/TransactionSigner.ts b/packages/networks/ton/src/services/TransactionSigner.ts index dd836b9..3fa5138 100644 --- a/packages/networks/ton/src/services/TransactionSigner.ts +++ b/packages/networks/ton/src/services/TransactionSigner.ts @@ -1,14 +1,14 @@ import { Provider } from '../services/Provider' import { mnemonicToPrivateKey } from '@ton/crypto' -import type { OpenedContract, WalletContractV5R1 } from '@ton/ton' import { type Cell, SendMode, type MessageRelaxed } from '@ton/core' +import type { OpenedContract, WalletContractV4, WalletContractV5R1 } from '@ton/ton' import type { PrivateKey, TransactionId, TransactionSignerInterface } from '@multiplechain/types' -export class TransactionSigner implements TransactionSignerInterface { +export class TransactionSigner implements TransactionSignerInterface { /** * Transaction data from the blockchain network */ - rawData: MessageRelaxed + rawData: MessageRelaxed[] /** * Signed transaction data @@ -23,14 +23,14 @@ export class TransactionSigner implements TransactionSignerInterface + wallet: OpenedContract /** * @param rawData - Transaction data * @param provider - Blockchain network provider */ constructor(rawData: MessageRelaxed, provider?: Provider) { - this.rawData = rawData + this.rawData = [rawData] this.provider = provider ?? Provider.instance } @@ -47,7 +47,26 @@ export class TransactionSigner implements TransactionSignerInterface { + const { publicKey, secretKey } = await mnemonicToPrivateKey(privateKey.split(' ')) + const contract = this.provider.createWalletV4(publicKey) + this.wallet = this.provider.client1.open(contract) + const seqno = await this.wallet.getSeqno() + this.signedData = this.wallet.createTransfer({ + seqno, + secretKey, + messages: this.rawData, sendMode: SendMode.PAY_GAS_SEPARATELY }) return this @@ -70,10 +89,20 @@ export class TransactionSigner implements TransactionSignerInterface { + module.default() +}) diff --git a/packages/networks/xrpl/index.example.html b/packages/networks/xrpl/index.example.html new file mode 100644 index 0000000..be17847 --- /dev/null +++ b/packages/networks/xrpl/index.example.html @@ -0,0 +1,309 @@ + + + + + + + Browser Tests + + + + +
+
    +
    + +
    +
    Adapter id:
    +
    Adapter name:
    +
    + Adapter icon: + icon +
    +
    Platforms:
    +
    Download link:
    +
    Deep link:
    +
    + Connected address: +
    + +
    Result:
    + +
    Result:
    +
    + + + + diff --git a/packages/networks/xrpl/package-lock.json b/packages/networks/xrpl/package-lock.json new file mode 100644 index 0000000..312fc86 --- /dev/null +++ b/packages/networks/xrpl/package-lock.json @@ -0,0 +1,772 @@ +{ + "name": "@multiplechain/bitcoin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@multiplechain/bitcoin", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@multiplechain/types": "^0.1.55", + "@multiplechain/utils": "^0.1.18", + "axios": "^1.6.8", + "bitcore-lib": "^10.0.28", + "ws": "^8.17.0" + }, + "devDependencies": { + "@types/bitcore-lib": "^0.15.6" + } + }, + "node_modules/@multiplechain/types": { + "version": "0.1.55", + "resolved": "https://registry.npmjs.org/@multiplechain/types/-/types-0.1.55.tgz", + "integrity": "sha512-9fYrLaDxX2pj9zfIbmvk1hPF3FH/bK+Q3uTF+k3zdsitP/HzQ1S9zImNz20Tvoqkk1ns+/8ecE0Y6GNowsDOQA==" + }, + "node_modules/@multiplechain/utils": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@multiplechain/utils/-/utils-0.1.18.tgz", + "integrity": "sha512-UCoOOBJrawp/lInepxuoEiYwId0wMPg7rX8p78FLqzGl8f/b93sAtv7sP87/VVKwhdoEuNZ/uQjckycmNeks/w==", + "dependencies": { + "@types/ws": "^8.5.10", + "bignumber.js": "^9.1.2", + "web3-utils": "^4.2.0", + "ws": "^8.16.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", + "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "dependencies": { + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", + "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", + "dependencies": { + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/bitcore-lib": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/@types/bitcore-lib/-/bitcore-lib-0.15.6.tgz", + "integrity": "sha512-CtKDBgSBubPXZ0wFeCiUCSdzH+cuy6nFya3FboOqf44evi+OmkQPqEg3ASMpmPDYE8vkcxV302Iu8lZqCjYieg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.12.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", + "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/bigi": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", + "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/bip-schnorr": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.6.4.tgz", + "integrity": "sha512-dNKw7Lea8B0wMIN4OjEmOk/Z5qUGqoPDY0P2QttLqGk1hmDPytLWW8PR5Pb6Vxy6CprcdEgfJpOjUu+ONQveyg==", + "dependencies": { + "bigi": "^1.4.2", + "ecurve": "^1.0.6", + "js-sha256": "^0.9.0", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bitcore-lib": { + "version": "10.0.28", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-10.0.28.tgz", + "integrity": "sha512-uOWHpWbUxEj411p+tp6DCb9NfZdsCAHl6Z/rs96mquQMsef29fOqWUyk4Bl7yLf+KwzU5ZKy0HIPnjGFlmpXpg==", + "dependencies": { + "bech32": "=2.0.0", + "bip-schnorr": "=0.6.4", + "bn.js": "=4.11.8", + "bs58": "^4.0.1", + "buffer-compare": "=1.1.1", + "elliptic": "^6.5.3", + "inherits": "=2.0.1", + "lodash": "^4.17.20" + } + }, + "node_modules/bitcore-lib/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==" + }, + "node_modules/bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer-compare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", + "integrity": "sha512-O6NvNiHZMd3mlIeMDjP6t/gPG75OqGPeiRZXoMQZJ6iy9GofCls4Ijs5YkPZZwoysizLiedhticmdyx/GyHghA==" + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecurve": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", + "integrity": "sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==", + "dependencies": { + "bigi": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/elliptic": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", + "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz", + "integrity": "sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==", + "dependencies": { + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@scure/bip32": "1.3.3", + "@scure/bip39": "1.2.2" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/web3-errors": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/web3-errors/-/web3-errors-1.1.4.tgz", + "integrity": "sha512-WahtszSqILez+83AxGecVroyZsMuuRT+KmQp4Si5P4Rnqbczno1k748PCrZTS1J4UCPmXMG2/Vt+0Bz2zwXkwQ==", + "dependencies": { + "web3-types": "^1.3.1" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-types": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/web3-types/-/web3-types-1.6.0.tgz", + "integrity": "sha512-qgOtADqlD5hw+KPKBUGaXAcdNLL0oh6qTeVgXwewCfbL/lG9R+/GrgMQB1gbTJ3cit8hMwtH8KX2Em6OwO0HRw==", + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-utils": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-4.2.3.tgz", + "integrity": "sha512-m5plKTC2YtQntHITQRyIePw52UVP1IrShhmA2FACtn4zmc5ADmrXOlQWiPzxFP/18eRJsAaUAw2+CQn1u4WPxQ==", + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "eventemitter3": "^5.0.1", + "web3-errors": "^1.1.4", + "web3-types": "^1.6.0", + "web3-validator": "^2.0.5" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/web3-validator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/web3-validator/-/web3-validator-2.0.5.tgz", + "integrity": "sha512-2gLOSW8XqEN5pw5jVUm20EB7A8SbQiekpAtiI0JBmCIV0a2rp97v8FgWY5E3UEqnw5WFfEqvcDVW92EyynDTyQ==", + "dependencies": { + "ethereum-cryptography": "^2.0.0", + "util": "^0.12.5", + "web3-errors": "^1.1.4", + "web3-types": "^1.5.0", + "zod": "^3.21.4" + }, + "engines": { + "node": ">=14", + "npm": ">=6.12.0" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/networks/xrpl/package.json b/packages/networks/xrpl/package.json new file mode 100644 index 0000000..a2905c0 --- /dev/null +++ b/packages/networks/xrpl/package.json @@ -0,0 +1,82 @@ +{ + "name": "@multiplechain/xrpl", + "version": "0.1.0", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.es.js", + "unpkg": "dist/index.umd.js", + "browser": "dist/index.umd.js", + "jsdelivr": "dist/index.umd.js", + "exports": { + ".": { + "import": { + "default": "./dist/index.es.js", + "types": "./dist/browser/index.d.ts" + }, + "require": { + "default": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "./node": { + "default": "./dist/index.cjs", + "types": "./dist/index.d.ts" + }, + "./browser": { + "default": "./dist/index.es.js", + "types": "./dist/browser/index.d.ts" + } + }, + "typesVersions": { + "*": { + "node": [ + "./dist/index.d.ts" + ], + "browser": [ + "./dist/browser/index.d.ts" + ] + } + }, + "files": [ + "dist", + "README.md", + "!tsconfig.tsbuildinfo" + ], + "scripts": { + "dev": "vite", + "clean": "rm -rf dist", + "watch": "tsc --watch", + "build:vite": "vite build", + "build:node": "tsx esbuild.ts", + "typecheck": "tsc --noEmit", + "lint": "eslint . --ext .ts", + "test": "vitest run --dir tests", + "test-ui": "vitest watch --ui", + "prepublishOnly": "pnpm run build", + "build": "pnpm run build:vite && pnpm run build:node" + }, + "keywords": [ + "web3", + "crypto", + "blockchain", + "multiple-chain" + ], + "author": "MultipleChain", + "license": "MIT", + "homepage": "https://github.com/MultipleChain/js/tree/master/packages/networks/xrpl", + "repository": { + "type": "git", + "url": "git+https://github.com/MultipleChain/js.git" + }, + "bugs": { + "url": "https://github.com/MultipleChain/js/issues" + }, + "dependencies": { + "@gemwallet/api": "^3.8.0", + "@multiplechain/types": "^0.1.70", + "@multiplechain/utils": "^0.1.21", + "add": "^2.0.6", + "axios": "^1.7.9", + "xrpl": "^4.1.0" + } +} \ No newline at end of file diff --git a/packages/networks/xrpl/pnpm-lock.yaml b/packages/networks/xrpl/pnpm-lock.yaml new file mode 100644 index 0000000..d8cedc4 --- /dev/null +++ b/packages/networks/xrpl/pnpm-lock.yaml @@ -0,0 +1,602 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@gemwallet/api': + specifier: ^3.8.0 + version: 3.8.0 + '@multiplechain/types': + specifier: ^0.1.70 + version: 0.1.70 + '@multiplechain/utils': + specifier: ^0.1.21 + version: 0.1.23 + add: + specifier: ^2.0.6 + version: 2.0.6 + axios: + specifier: ^1.7.9 + version: 1.7.9 + xrpl: + specifier: ^4.1.0 + version: 4.1.0 + +packages: + + '@gemwallet/api@3.8.0': + resolution: {integrity: sha512-hZ6XC0mVm3Q54cgonrzk6tHS/wUMjtPHyqsqbtlnNGPouCR7OIfEDo5Y802qLZ5ah6PskhsK0DouVnwUykEM8Q==} + + '@multiplechain/types@0.1.70': + resolution: {integrity: sha512-o9ovdaefDHE5gorb83avugCjeixfBXrfJkIgSEEcT549yGF8CkmG/jgZIlqXKJf8Lh0tbg4zMAAanUSxUqV1FQ==} + + '@multiplechain/utils@0.1.23': + resolution: {integrity: sha512-6mgJiXQsElObKgX/yA8DUpQYd+BS1GKhAyLj5F/2mv9yY3boZrX8sS7ZLk+g5NX45N+BACNMxbs09TG1iFiRsA==} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@types/node@22.12.0': + resolution: {integrity: sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==} + + '@types/ws@8.5.14': + resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + + '@xrplf/isomorphic@1.0.1': + resolution: {integrity: sha512-0bIpgx8PDjYdrLFeC3csF305QQ1L7sxaWnL5y71mCvhenZzJgku9QsA+9QCXBC1eNYtxWO/xR91zrXJy2T/ixg==} + engines: {node: '>=16.0.0'} + + '@xrplf/secret-numbers@1.0.0': + resolution: {integrity: sha512-qsCLGyqe1zaq9j7PZJopK+iGTGRbk6akkg6iZXJJgxKwck0C5x5Gnwlb1HKYGOwPKyrXWpV6a2YmcpNpUFctGg==} + + add@2.0.6: + resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.4: + resolution: {integrity: sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==} + engines: {node: '>= 0.4'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + ripple-address-codec@5.0.0: + resolution: {integrity: sha512-de7osLRH/pt5HX2xw2TRJtbdLLWHu0RXirpQaEeCnWKY5DYHykh3ETSkofvm0aX0LJiV7kwkegJxQkmbO94gWw==} + engines: {node: '>= 16'} + + ripple-binary-codec@2.2.0: + resolution: {integrity: sha512-93fvAW3oXux4NY5Xf79dUIOhud5DmyEcC5RrTYdl0BPaYOGXC/txCQl9FnwIVZGkVMtZFLcFwJfmH1zFgfRyKA==} + engines: {node: '>= 18'} + + ripple-keypairs@2.0.0: + resolution: {integrity: sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag==} + engines: {node: '>= 16'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + web3-errors@1.3.1: + resolution: {integrity: sha512-w3NMJujH+ZSW4ltIZZKtdbkbyQEvBzyp3JRn59Ckli0Nz4VMsVq8aF1bLWM7A2kuQ+yVEm3ySeNU+7mSRwx7RQ==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-types@1.10.0: + resolution: {integrity: sha512-0IXoaAFtFc8Yin7cCdQfB9ZmjafrbP6BO0f0KT/khMhXKUpoJ6yShrVhiNpyRBo8QQjuOagsWzwSK2H49I7sbw==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-utils@4.3.3: + resolution: {integrity: sha512-kZUeCwaQm+RNc2Bf1V3BYbF29lQQKz28L0y+FA4G0lS8IxtJVGi5SeDTUkpwqqkdHHC7JcapPDnyyzJ1lfWlOw==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-validator@2.0.6: + resolution: {integrity: sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg==} + engines: {node: '>=14', npm: '>=6.12.0'} + + which-typed-array@1.1.18: + resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + engines: {node: '>= 0.4'} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xrpl@4.1.0: + resolution: {integrity: sha512-H/+BCEnFLyQOBUC6h4nMKg7I9AuxHe4kj9ZwQHX2zoL9n/ZOERc6B2U079pogI84zCbYdUWIn4DkoIYvjO/hpg==} + engines: {node: '>=18.0.0'} + + zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + +snapshots: + + '@gemwallet/api@3.8.0': {} + + '@multiplechain/types@0.1.70': {} + + '@multiplechain/utils@0.1.23': + dependencies: + '@types/ws': 8.5.14 + bignumber.js: 9.1.2 + web3-utils: 4.3.3 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/hashes@1.4.0': {} + + '@scure/base@1.1.9': {} + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@types/node@22.12.0': + dependencies: + undici-types: 6.20.0 + + '@types/ws@8.5.14': + dependencies: + '@types/node': 22.12.0 + + '@xrplf/isomorphic@1.0.1': + dependencies: + '@noble/hashes': 1.4.0 + eventemitter3: 5.0.1 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@xrplf/secret-numbers@1.0.0': + dependencies: + '@xrplf/isomorphic': 1.0.1 + ripple-keypairs: 2.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + add@2.0.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + bignumber.js@9.1.2: {} + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + eventemitter3@5.0.1: {} + + follow-redirects@1.15.9: {} + + for-each@0.3.4: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + function-bind@1.1.2: {} + + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + inherits@2.0.4: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.18 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + possible-typed-array-names@1.0.0: {} + + proxy-from-env@1.1.0: {} + + ripple-address-codec@5.0.0: + dependencies: + '@scure/base': 1.1.9 + '@xrplf/isomorphic': 1.0.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ripple-binary-codec@2.2.0: + dependencies: + '@xrplf/isomorphic': 1.0.1 + bignumber.js: 9.1.2 + ripple-address-codec: 5.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ripple-keypairs@2.0.0: + dependencies: + '@noble/curves': 1.4.2 + '@xrplf/isomorphic': 1.0.1 + ripple-address-codec: 5.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + undici-types@6.20.0: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.18 + + web3-errors@1.3.1: + dependencies: + web3-types: 1.10.0 + + web3-types@1.10.0: {} + + web3-utils@4.3.3: + dependencies: + ethereum-cryptography: 2.2.1 + eventemitter3: 5.0.1 + web3-errors: 1.3.1 + web3-types: 1.10.0 + web3-validator: 2.0.6 + + web3-validator@2.0.6: + dependencies: + ethereum-cryptography: 2.2.1 + util: 0.12.5 + web3-errors: 1.3.1 + web3-types: 1.10.0 + zod: 3.24.1 + + which-typed-array@1.1.18: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + for-each: 0.3.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + ws@8.18.0: {} + + xrpl@4.1.0: + dependencies: + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + '@xrplf/isomorphic': 1.0.1 + '@xrplf/secret-numbers': 1.0.0 + bignumber.js: 9.1.2 + eventemitter3: 5.0.1 + ripple-address-codec: 5.0.0 + ripple-binary-codec: 2.2.0 + ripple-keypairs: 2.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + zod@3.24.1: {} diff --git a/packages/networks/xrpl/src/assets/Coin.ts b/packages/networks/xrpl/src/assets/Coin.ts new file mode 100644 index 0000000..74c5ebb --- /dev/null +++ b/packages/networks/xrpl/src/assets/Coin.ts @@ -0,0 +1,97 @@ +import { dropsToXrp, xrpToDrops } from 'xrpl' +import { Provider } from '../services/Provider' +import { TransactionSigner } from '../services/TransactionSigner' +import { + ErrorTypeEnum, + type CoinInterface, + type TransferAmount, + type WalletAddress +} from '@multiplechain/types' + +export class Coin implements CoinInterface { + /** + * Blockchain network provider + */ + provider: Provider + + /** + * @param provider network provider + */ + constructor(provider?: Provider) { + this.provider = provider ?? Provider.instance + } + + /** + * @returns Coin name + */ + getName(): string { + return 'XRP' + } + + /** + * @returns Coin symbol + */ + getSymbol(): string { + return 'XRP' + } + + /** + * @returns Decimal value of the coin + */ + getDecimals(): number { + return 6 + } + + /** + * @param owner Wallet address + * @returns Wallet balance as currency of COIN + */ + async getBalance(owner: WalletAddress): Promise { + return dropsToXrp(await this.provider.rpc.getBalance(owner)) + } + + /** + * @param sender Sender wallet address + * @param receiver Receiver wallet address + * @param amount Amount of assets that will be transferred + * @param memo Memo for the transaction + * @returns Transaction signer + */ + async transfer( + sender: WalletAddress, + receiver: WalletAddress, + amount: TransferAmount, + memo?: string + ): Promise { + if (amount < 0) { + throw new Error(ErrorTypeEnum.INVALID_AMOUNT) + } + + if (amount > (await this.getBalance(sender))) { + throw new Error(ErrorTypeEnum.INSUFFICIENT_BALANCE) + } + + if (sender === receiver) { + throw new Error(ErrorTypeEnum.INVALID_ADDRESS) + } + + const info = await this.provider.rpc.getAccountInfo(receiver) + + if (this.provider.rpc.isError(info) && info.error === 'actNotFound') { + const minReserve = await this.provider.rpc.getMinimumReserve() + if (amount < minReserve) { + throw new Error( + `This account is not activated, so you have to send at least ${minReserve} XRP to activate it` + ) + } + } + + return new TransactionSigner({ + Account: sender, + Destination: receiver, + Amount: xrpToDrops(amount), + TransactionType: 'Payment', + Memos: memo ? [this.provider.createMemo(memo)] : [] + }) + } +} diff --git a/packages/networks/xrpl/src/assets/index.ts b/packages/networks/xrpl/src/assets/index.ts new file mode 100644 index 0000000..c03964d --- /dev/null +++ b/packages/networks/xrpl/src/assets/index.ts @@ -0,0 +1 @@ +export * from './Coin' diff --git a/packages/networks/xrpl/src/browser/Wallet.ts b/packages/networks/xrpl/src/browser/Wallet.ts new file mode 100644 index 0000000..dd8a7d1 --- /dev/null +++ b/packages/networks/xrpl/src/browser/Wallet.ts @@ -0,0 +1,195 @@ +import { + type WalletInterface, + type WalletAdapterInterface, + type WalletPlatformEnum, + type UnknownConfig, + type ConnectConfig, + type WalletAddress, + type SignedMessage, + type TransactionId, + ErrorTypeEnum +} from '@multiplechain/types' +import { Provider } from '../services/Provider' +import type { TransactionSigner } from '../services/TransactionSigner' + +const rejectMap = (error: any, reject: (a: any) => any): any => { + console.error('MultipleChain XRPl Wallet Error:', error) + + const errorMessage = String(error.message ?? '') + + if (errorMessage.includes('User rejected the request')) { + return reject(new Error(ErrorTypeEnum.WALLET_REQUEST_REJECTED)) + } + + if (errorMessage.includes('Error calling submit')) { + return reject(new Error(ErrorTypeEnum.TRANSACTION_CREATION_FAILED)) + } + + return reject(error) +} + +export interface WalletProvider { + getAddress: () => Promise + signMessage: (message: string) => Promise + sendXrp: (to: string, amount: string) => Promise + on: (event: string, callback: (data: any) => void) => void +} + +type WalletAdapter = WalletAdapterInterface + +export class Wallet implements WalletInterface { + adapter: WalletAdapter + + walletProvider: WalletProvider + + networkProvider: Provider + + /** + * @param adapter - Wallet adapter + * @param provider - Network provider + */ + constructor(adapter: WalletAdapter, provider?: Provider) { + this.adapter = adapter + this.networkProvider = provider ?? Provider.instance + } + + /** + * @returns Wallet ID + */ + getId(): string { + return this.adapter.id + } + + /** + * @returns Wallet name + */ + getName(): string { + return this.adapter.name + } + + /** + * @returns Wallet icon + */ + getIcon(): string { + return this.adapter.icon + } + + /** + * @returns Wallet platforms + */ + getPlatforms(): WalletPlatformEnum[] { + return this.adapter.platforms + } + + /** + * @returns Wallet download link + */ + getDownloadLink(): string | undefined { + return this.adapter.downloadLink + } + + /** + * @param url url for deep linking + * @param config configuration for deep linking + * @returns deep link + */ + createDeepLink(url: string, config?: UnknownConfig): string | null { + if (this.adapter.createDeepLink === undefined) { + return null + } + + return this.adapter.createDeepLink(url, config) + } + + /** + * @param config connection configuration + * @returns WalletAddress + */ + async connect(config?: ConnectConfig): Promise { + return await new Promise((resolve, reject) => { + this.adapter + .connect(this.networkProvider, config) + .then(async (provider) => { + this.walletProvider = provider + resolve(await this.getAddress()) + }) + .catch((error) => { + const customReject = (error: any): void => { + if (error.message === ErrorTypeEnum.WALLET_REQUEST_REJECTED) { + reject(new Error(ErrorTypeEnum.WALLET_CONNECT_REJECTED)) + } else { + reject(error) + } + } + rejectMap(error, customReject) + }) + }) + } + + /** + * @returns wallet detection status + */ + async isDetected(): Promise { + return await this.adapter.isDetected() + } + + /** + * @returns connection status + */ + async isConnected(): Promise { + return await this.adapter.isConnected() + } + + /** + * @returns wallet address + */ + async getAddress(): Promise { + return await this.walletProvider.getAddress() + } + + /** + * @param message message to sign + * @returns signed message + */ + async signMessage(message: string): Promise { + return await new Promise((resolve, reject) => { + this.walletProvider + .signMessage(message) + .then((signature) => { + resolve(signature) + }) + .catch((error) => { + rejectMap(error, reject) + }) + }) + } + + /** + * @param transactionSigner transaction signer + * @returns transaction id + */ + async sendTransaction(transactionSigner: TransactionSigner): Promise { + const data = transactionSigner.getRawData() + return await new Promise((resolve, reject) => { + if (!data.Destination || !data.Amount) { + throw new Error('Invalid transaction data') + } + this.walletProvider + .sendXrp(data.Destination, data.Amount) + .then((txHash) => { + resolve(txHash) + }) + .catch((error) => { + rejectMap(error, reject) + }) + }) + } + + /** + * @param eventName event name + * @param callback event callback + */ + on(eventName: string, callback: (...args: any[]) => void): void { + this.walletProvider.on(eventName, callback) + } +} diff --git a/packages/networks/xrpl/src/browser/adapters/EIP6963.ts b/packages/networks/xrpl/src/browser/adapters/EIP6963.ts new file mode 100644 index 0000000..861bbcd --- /dev/null +++ b/packages/networks/xrpl/src/browser/adapters/EIP6963.ts @@ -0,0 +1,25 @@ +export interface EIP1193Provider { + request: (payload: { method: string; params?: any[] | object }) => Promise + on: (eventName: string, callback: (...args: any[]) => void) => void +} + +export interface EIP6963ProviderInfo { + uuid: string + name: string + icon: string + rdns?: string +} + +export interface EIP6963ProviderDetail { + info: EIP6963ProviderInfo + provider: EIP1193Provider +} + +export interface EVMProviderDetected extends EIP6963ProviderDetail { + accounts: string[] + request?: EIP1193Provider['request'] +} + +export interface EIP6963AnnounceProviderEvent extends Event { + detail: EIP6963ProviderDetail +} diff --git a/packages/networks/xrpl/src/browser/adapters/GemWallet.ts b/packages/networks/xrpl/src/browser/adapters/GemWallet.ts new file mode 100644 index 0000000..455d734 --- /dev/null +++ b/packages/networks/xrpl/src/browser/adapters/GemWallet.ts @@ -0,0 +1,118 @@ +import { gemWallet } from './icons' +import type { WalletProvider } from '../Wallet' +import type { Provider } from '../../services/Provider' +import type { WalletAdapterInterface } from '@multiplechain/types' +import { ErrorTypeEnum, WalletPlatformEnum } from '@multiplechain/types' +import { getAddress, getNetwork, isInstalled, on, sendPayment, signMessage } from '@gemwallet/api' + +declare global { + interface Window { + gemWallet: boolean + } +} + +let address: string | undefined + +const GemWallet: WalletAdapterInterface = { + id: 'gem-wallet', + name: 'GemWallet', + icon: gemWallet, + downloadLink: + 'https://chromewebstore.google.com/detail/gemwallet/egebedonbdapoieedfcfkofloclfghab', + platforms: [WalletPlatformEnum.UNIVERSAL], + isDetected: () => true, + isConnected: () => window?.gemWallet, + connect: async (provider?: Provider): Promise => { + return await new Promise((resolve, reject) => { + if (provider === undefined) { + throw new Error(ErrorTypeEnum.PROVIDER_IS_REQUIRED) + } + + const chainId = provider !== undefined && provider?.isTestnet() ? 'Testnet' : 'Mainnet' + + try { + const walletProvider: WalletProvider = { + getAddress: async (): Promise => { + return address ?? '' + }, + signMessage: async (message: string): Promise => { + return await new Promise((resolve, reject) => { + signMessage(message) + .then(({ result }) => { + if (result?.signedMessage) { + resolve(result.signedMessage) + } else { + reject(new Error('Signing failed')) + } + }) + .catch(reject) + }) + }, + sendXrp: async (to: string, amount: string): Promise => { + return await new Promise((resolve, reject) => { + sendPayment({ + destination: to, + amount + }) + .then((response) => { + if (response.result?.hash) { + resolve(response.result.hash) + } else { + if (response.type === 'reject') { + reject(new Error(ErrorTypeEnum.WALLET_REQUEST_REJECTED)) + } else { + reject( + new Error(ErrorTypeEnum.TRANSACTION_CREATION_FAILED) + ) + } + } + }) + .catch(reject) + }) + }, + on + } + + const connect = async (): Promise => { + void isInstalled() + .then((res) => { + if (res.result.isInstalled) { + getAddress() + .then(({ result }) => { + if (result?.address) { + address = result.address + void getNetwork() + .then(({ result }) => { + if (result?.network === chainId) { + resolve(walletProvider) + } else { + reject( + new Error( + ErrorTypeEnum.UNACCEPTED_CHAIN + ) + ) + } + }) + .catch(reject) + } else { + reject( + new Error(ErrorTypeEnum.WALLET_CONNECTION_FAILED) + ) + } + }) + .catch(reject) + } else { + reject(new Error('GemWallet is not installed')) + } + }) + .catch(reject) + } + void connect() + } catch (error) { + reject(error) + } + }) + } +} + +export default GemWallet diff --git a/packages/networks/xrpl/src/browser/adapters/MetaMask.ts b/packages/networks/xrpl/src/browser/adapters/MetaMask.ts new file mode 100644 index 0000000..2ffc73f --- /dev/null +++ b/packages/networks/xrpl/src/browser/adapters/MetaMask.ts @@ -0,0 +1,170 @@ +import { metaMask } from './icons' +import type { WalletProvider } from '../Wallet' +import type { EIP1193Provider } from './EIP6963' +import type { Provider } from '../../services/Provider' +import { ErrorTypeEnum, WalletPlatformEnum } from '@multiplechain/types' +import type { WalletAdapterInterface } from '@multiplechain/types' + +export interface WindowEthereum extends EIP1193Provider { + isTrust?: boolean + isTronLink?: boolean + isMetaMask?: boolean +} + +declare global { + interface Window { + ethereum: WindowEthereum + } +} + +interface NetworkInfo { + chainId: number + explorerUrl: string + name: string + nodeUrl: string +} + +const MetaMask: WalletAdapterInterface = { + id: 'metamask', + name: 'MetaMask Snap', + icon: metaMask, + downloadLink: 'https://metamask.io/download/', + platforms: [WalletPlatformEnum.BROWSER, WalletPlatformEnum.MOBILE], + isDetected: () => { + return Boolean((window?.ethereum as unknown as WindowEthereum)?.isMetaMask) + }, + createDeepLink: (url: string): string => `https://metamask.app.link/dapp/${url}`, + isConnected: async () => { + return Boolean( + ( + await (window?.ethereum as unknown as WindowEthereum).request({ + method: 'eth_accounts' + }) + ).length + ) + }, + connect: async (provider?: Provider): Promise => { + return await new Promise((resolve, reject) => { + if (provider === undefined) { + throw new Error(ErrorTypeEnum.PROVIDER_IS_REQUIRED) + } + + const chainId = provider !== undefined && provider?.isTestnet() ? 1 : 0 + + const metamaskProvider = window?.ethereum as unknown as WindowEthereum + try { + const walletProvider: WalletProvider = { + getAddress: async (): Promise => { + const result = await metamaskProvider.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:xrpl-snap', + request: { + method: 'xrpl_getAccount' + } + } + }) + return result.account + }, + signMessage: async (message: string): Promise => { + const { signature } = await metamaskProvider.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:xrpl-snap', + request: { + method: 'xrpl_signMessage', + params: { + message + } + } + } + }) + return signature + }, + sendXrp: async (to: string, amount: string): Promise => { + const result = await metamaskProvider.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:xrpl-snap', + request: { + method: 'xrpl_sign', + params: { + Amount: amount, + Destination: to, + TransactionType: 'Payment', + Account: await walletProvider.getAddress() + } + } + } + }) + await provider.ws.connect() + await provider.ws.submit(result.tx_blob) // eslint-disable-line + await provider.ws.disconnect() + return result.hash + }, + on: (event: string, callback: (data: any) => void) => { + metamaskProvider.on(event, callback) + } + } + + const getCurrentNetwork = async (): Promise => { + return await metamaskProvider.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:xrpl-snap', + request: { + method: 'xrpl_getActiveNetwork' + } + } + }) + } + + const changeNetwork = async (chainId: number): Promise => { + try { + await metamaskProvider.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:xrpl-snap', + request: { + method: 'xrpl_changeNetwork', + params: { + chainId + } + } + } + }) + } catch (error) { + reject(error) + } + } + + const connect = async (): Promise => { + try { + await metamaskProvider.request({ + method: 'wallet_requestSnaps', + params: { + 'npm:xrpl-snap': {} + } + }) + + void getCurrentNetwork().then(async (network) => { + if (network.chainId !== chainId) { + await changeNetwork(chainId) + } + + resolve(walletProvider) + }) + } catch (error) { + reject(error) + } + } + + void connect() + } catch (error) { + reject(error) + } + }) + } +} + +export default MetaMask diff --git a/packages/networks/xrpl/src/browser/adapters/icons.ts b/packages/networks/xrpl/src/browser/adapters/icons.ts new file mode 100644 index 0000000..22c71b9 --- /dev/null +++ b/packages/networks/xrpl/src/browser/adapters/icons.ts @@ -0,0 +1,5 @@ +export const metaMask = + 'data:image/png;base64,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' + +export const gemWallet = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0iIzAwQThFQSIgZD0iTTIwIDM5LjkxMS41OTMgMTcuNDIyaDM4LjgxNHoiLz48cGF0aCBmaWxsPSIjMzNEM0Y0IiBkPSJNMzMuMTg1IDUuMzMzSDYuODE1TC41OTMgMTcuNDIzaDM4LjgxNHoiLz48cGF0aCBmaWxsPSIjNDBFRUZGIiBkPSJtMjAgMzkuOTExLTcuMDM3LTIyLjQ4OWgxNC4wNzR6TTE0LjIyMiAxNC40IDguNjY3IDUuMzMzSDIwem0xMS4yNTkgMEwyMCA1LjMzM2gxMS4zMzN6Ii8+PHBhdGggZmlsbD0iI0ZGRiIgZD0iTTYuMjk2IDYuNDg5IDMuNDA3IDUuMzMzbDIuODktMS4xNTVMNy4yNTguNzFsLjk2MyAzLjQ2NyAyLjg5IDEuMTU1LTIuODkgMS4xNTYtLjk2MyAzLjQ2N3ptMjcuNDA4IDExLjI4OS0xLjg1Mi0uNzExIDEuODUyLS44LjY2Ni0yLjIyMy41OTMgMi4yMjMgMS44NTIuOC0xLjg1Mi43MUwzNC4zNyAyMHoiLz48cGF0aCBkPSJNMjEuODUyIDUuMzMzIDYuMjk2IDI0LjA5bC0xLjMzMy0xLjUxMSAxNC4zNy0xNy4yNDV6bTguNzQxIDBMMTAuNzQgMjkuMTU2IDcuNjMgMjUuNiAyNC40NDQgNS4zMzN6IiBvcGFjaXR5PSIuMiIgZmlsbD0iI0ZGRiIvPjwvZz48L3N2Zz4=' diff --git a/packages/networks/xrpl/src/browser/adapters/index.ts b/packages/networks/xrpl/src/browser/adapters/index.ts new file mode 100644 index 0000000..ca6f997 --- /dev/null +++ b/packages/networks/xrpl/src/browser/adapters/index.ts @@ -0,0 +1,2 @@ +export { default as MetaMask } from './MetaMask' +export { default as GemWallet } from './GemWallet' diff --git a/packages/networks/xrpl/src/browser/index.ts b/packages/networks/xrpl/src/browser/index.ts new file mode 100644 index 0000000..a054aab --- /dev/null +++ b/packages/networks/xrpl/src/browser/index.ts @@ -0,0 +1,28 @@ +import { Wallet, type WalletProvider } from './Wallet' +import type { Provider } from '../services/Provider' +import * as adapterList from './adapters/index' +import type { + WalletAdapterListType, + WalletAdapterInterface, + RegisterWalletAdapterType +} from '@multiplechain/types' + +const adapters: WalletAdapterListType = {} + +const registerAdapter: RegisterWalletAdapterType = ( + adapter: WalletAdapterInterface +): void => { + if (Object.values(adapters).find((a) => a.id === adapter.id) !== undefined) { + throw new Error(`Adapter with id ${adapter.id} already exists`) + } + + adapters[adapter.id] = adapter +} + +export * from '../index' + +export const browser = { + Wallet, + registerAdapter, + adapters: Object.assign(adapters, adapterList) +} diff --git a/packages/networks/xrpl/src/index.ts b/packages/networks/xrpl/src/index.ts new file mode 100644 index 0000000..19e38b2 --- /dev/null +++ b/packages/networks/xrpl/src/index.ts @@ -0,0 +1,7 @@ +export * from './services/Provider' + +export * as assets from './assets/index' +export * as models from './models/index' +export * as services from './services/index' + +export * as types from '@multiplechain/types' diff --git a/packages/networks/xrpl/src/models/CoinTransaction.ts b/packages/networks/xrpl/src/models/CoinTransaction.ts new file mode 100644 index 0000000..13a5fe4 --- /dev/null +++ b/packages/networks/xrpl/src/models/CoinTransaction.ts @@ -0,0 +1,63 @@ +import { dropsToXrp } from 'xrpl' +import { Transaction } from './Transaction' +import { TransactionStatusEnum, AssetDirectionEnum } from '@multiplechain/types' +import type { WalletAddress, CoinTransactionInterface, TransferAmount } from '@multiplechain/types' + +export class CoinTransaction extends Transaction implements CoinTransactionInterface { + /** + * @returns Wallet address of the receiver of transaction + */ + async getReceiver(): Promise { + const data = await this.getData() + return data?.Destination ?? '' + } + + /** + * @returns Wallet address of the sender of transaction + */ + async getSender(): Promise { + return await this.getSigner() + } + + /** + * @returns Amount of coin that will be transferred + */ + async getAmount(): Promise { + const data = await this.getData() + return dropsToXrp(data?.Amount ?? 0) + } + + /** + * @param direction - Direction of the transaction (asset) + * @param address - Wallet address of the receiver or sender of the transaction, dependant on direction + * @param amount Amount of assets that will be transferred + * @returns Status of the transaction + */ + async verifyTransfer( + direction: AssetDirectionEnum, + address: WalletAddress, + amount: TransferAmount + ): Promise { + const status = await this.getStatus() + + if (status === TransactionStatusEnum.PENDING) { + return TransactionStatusEnum.PENDING + } + + if ((await this.getAmount()) !== amount) { + return TransactionStatusEnum.FAILED + } + + if (direction === AssetDirectionEnum.INCOMING) { + if ((await this.getReceiver()).toLowerCase() !== address.toLowerCase()) { + return TransactionStatusEnum.FAILED + } + } else { + if ((await this.getSender()).toLowerCase() !== address.toLowerCase()) { + return TransactionStatusEnum.FAILED + } + } + + return TransactionStatusEnum.CONFIRMED + } +} diff --git a/packages/networks/xrpl/src/models/Transaction.ts b/packages/networks/xrpl/src/models/Transaction.ts new file mode 100644 index 0000000..ec31f98 --- /dev/null +++ b/packages/networks/xrpl/src/models/Transaction.ts @@ -0,0 +1,190 @@ +import { sleep } from '@multiplechain/utils' +import { Provider } from '../services/Provider' +import { ErrorTypeEnum, TransactionStatusEnum } from '@multiplechain/types' +import { dropsToXrp, type Transaction as BaseTransactionData, type TransactionMetadata } from 'xrpl' +import { + TransactionTypeEnum, + type BlockConfirmationCount, + type BlockNumber, + type BlockTimestamp, + type TransactionFee, + type TransactionId, + type TransactionInterface, + type WalletAddress +} from '@multiplechain/types' + +export type TransactionData = BaseTransactionData & { + meta?: TransactionMetadata + Destination?: string + ledger_index?: number + Amount?: number + date?: number +} + +let counter = 0 + +export class Transaction implements TransactionInterface { + /** + * Each transaction has its own unique ID defined by the user + */ + id: TransactionId + + /** + * Blockchain network provider + */ + provider: Provider + + /** + * Transaction data + */ + data: TransactionData | null = null + + /** + * @param id Transaction id + * @param provider Blockchain network provider + */ + constructor(id: TransactionId, provider?: Provider) { + this.id = id + this.provider = provider ?? Provider.instance + } + + /** + * @returns Transaction data + */ + async getData(): Promise { + if (this.data?.meta) { + return this.data + } + try { + return (this.data = await this.provider.rpc.getTransaction(this.id)) + } catch (error) { + console.error('MC XRPl TX getData', error) + // Returns empty data when the transaction is first created. For this reason, it would be better to check it intermittently and give an error if it still does not exist. Average 10 seconds. + if (String((error as any).message).includes('Transaction not found')) { + if (counter > 5) { + throw new Error(ErrorTypeEnum.TRANSACTION_NOT_FOUND) + } + counter++ + await sleep(2000) + return await this.getData() + } + throw new Error(ErrorTypeEnum.RPC_REQUEST_ERROR) + } + } + + /** + * @param ms - Milliseconds to wait for the transaction to be confirmed. Default is 4000ms + * @returns Status of the transaction + */ + async wait(ms: number = 4000): Promise { + return await new Promise((resolve, reject) => { + const check = async (): Promise => { + try { + const status = await this.getStatus() + if (status !== TransactionStatusEnum.PENDING) { + resolve(status) + } + setTimeout(check, ms) + } catch (error) { + console.error('MC XRPl TX wait', error) + reject(TransactionStatusEnum.FAILED) + } + } + void check() + }) + } + + /** + * @returns Transaction ID + */ + getId(): TransactionId { + return this.id + } + + /** + * @returns Transaction type + */ + async getType(): Promise { + return TransactionTypeEnum.COIN + } + + /** + * @returns Transaction URL + */ + getUrl(): string { + return this.provider.explorer + 'transactions/' + this.id + } + + async getMemos(): Promise { + const data = await this.getData() + return (data?.Memos ?? []).map(({ Memo }) => { + if (Memo.MemoData) { + Memo.MemoData = Buffer.from(Memo.MemoData, 'hex').toString('utf-8') + } + + if (Memo.MemoType) { + Memo.MemoType = Buffer.from(Memo.MemoType, 'hex').toString('utf-8') + } + + if (Memo.MemoFormat) { + Memo.MemoFormat = Buffer.from(Memo.MemoFormat, 'hex').toString('utf-8') + } + + return Memo + }) + } + + /** + * @returns Wallet address of the sender of transaction + */ + async getSigner(): Promise { + const data = await this.getData() + return data?.Account ?? '' + } + + /** + * @returns Transaction fee + */ + async getFee(): Promise { + const data = await this.getData() + return dropsToXrp(data?.Fee ?? 0) + } + + /** + * @returns Block number that transaction + */ + async getBlockNumber(): Promise { + const data = await this.getData() + return data?.ledger_index ?? 0 + } + + /** + * @returns Block timestamp that transaction + */ + async getBlockTimestamp(): Promise { + const data = await this.getData() + return data?.date ?? 0 + } + + /** + * @returns Confirmation count of the block + */ + async getBlockConfirmationCount(): Promise { + const blockNumber = await this.getBlockNumber() + const ledger = await this.provider.rpc.getLedger() + return ledger.result.ledger_index - blockNumber + } + + /** + * @returns Status of the transaction + */ + async getStatus(): Promise { + const data = await this.getData() + if (data?.meta) { + return data?.meta?.TransactionResult === 'tesSUCCESS' + ? TransactionStatusEnum.CONFIRMED + : TransactionStatusEnum.FAILED + } + return TransactionStatusEnum.PENDING + } +} diff --git a/packages/networks/xrpl/src/models/index.ts b/packages/networks/xrpl/src/models/index.ts new file mode 100644 index 0000000..4cca7f1 --- /dev/null +++ b/packages/networks/xrpl/src/models/index.ts @@ -0,0 +1,2 @@ +export * from './Transaction' +export * from './CoinTransaction' diff --git a/packages/networks/xrpl/src/services/Client.ts b/packages/networks/xrpl/src/services/Client.ts new file mode 100644 index 0000000..29905b4 --- /dev/null +++ b/packages/networks/xrpl/src/services/Client.ts @@ -0,0 +1,87 @@ +import axios from 'axios' +import type { TransactionData } from '../models' +import type { AccountInfoResponse, ErrorResponse, LedgerResponse } from 'xrpl' + +export default class Client { + private readonly rpcUrl: string + + constructor(rpcUrl: string) { + this.rpcUrl = rpcUrl + } + + async request(method: string, params: any): Promise { + try { + const response = await axios.post(this.rpcUrl, { + method, + params: [params] + }) + + if (response.status !== 200 || response.data.error) { + throw new Error(JSON.stringify(response.data)) + } + + return response.data + } catch (error) { + return error as Error + } + } + + async getMinimumReserve(): Promise { + const result = await this.request('server_info', {}) + + if (result instanceof Error) { + throw result + } + + return result.info.validated_ledger.reserve_base_xrp + } + + async getAccountInfo(address: string): Promise { + return await this.request('account_info', { + account: address, + ledger_index: 'validated' + }) + } + + isError(response: any): response is ErrorResponse { + return response && response.status === 'error' + } + + async getBalance(address: string): Promise { + const response = await this.getAccountInfo(address) + + if (this.isError(response)) { + return '0' + } + + return response.result.account_data.Balance + } + + async getLedger(): Promise { + return await this.request('ledger', { + ledger_index: 'validated' + }) + } + + async getFee(): Promise { + const response = await this.request('fee', {}) + + if (response instanceof Error) { + throw response + } + + return response.result.drops.minimum_fee + } + + async getTransaction(txId: string): Promise { + const { result } = await this.request('tx', { + transaction: txId + }) + + if (result.error) { + throw new Error(result.error_message as string) + } + + return result + } +} diff --git a/packages/networks/xrpl/src/services/Provider.ts b/packages/networks/xrpl/src/services/Provider.ts new file mode 100644 index 0000000..2a60f24 --- /dev/null +++ b/packages/networks/xrpl/src/services/Provider.ts @@ -0,0 +1,162 @@ +import Client from './Client' +import { Client as WsClient, type Memo } from 'xrpl' +import { checkWebSocket } from '@multiplechain/utils' +import { + ErrorTypeEnum, + type NetworkConfigInterface, + type ProviderInterface +} from '@multiplechain/types' + +export class Provider implements ProviderInterface { + /** + * Network configuration of the provider + */ + network: NetworkConfigInterface + + ws: WsClient + + rpc: Client + + explorer: string + + testnetRpc = 'https://s.altnet.rippletest.net:51234' + + testnetWs = 'wss://s.altnet.rippletest.net:51233' + + testnetRpc2 = + 'https://young-multi-liquid.xrp-testnet.quiknode.pro/d8af58c32952286972ad82bc5dc1156e70c4a2a0' + + testnetWs2 = + 'wss://young-multi-liquid.xrp-testnet.quiknode.pro/d8af58c32952286972ad82bc5dc1156e70c4a2a0' + + mainnetRpc = 'https://xrplcluster.com' + + mainnetWs = 'wss://xrplcluster.com' + + /** + * Static instance of the provider + */ + private static _instance: Provider + + /** + * @param network - Network configuration of the provider + */ + constructor(network: NetworkConfigInterface) { + this.update(network) + } + + /** + * Get the static instance of the provider + * @returns Provider + */ + static get instance(): Provider { + if (Provider._instance === undefined) { + throw new Error(ErrorTypeEnum.PROVIDER_IS_NOT_INITIALIZED) + } + return Provider._instance + } + + /** + * Initialize the static instance of the provider + * @param network - Network configuration of the provider + */ + static initialize(network: NetworkConfigInterface): void { + if (Provider._instance !== undefined) { + throw new Error(ErrorTypeEnum.PROVIDER_IS_ALREADY_INITIALIZED) + } + Provider._instance = new Provider(network) + } + + /** + * Check RPC connection + * @param url - RPC URL + * @returns Connection status + */ + async checkRpcConnection(url?: string): Promise { + try { + const response = await fetch(url ?? this.network.rpcUrl ?? '', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + method: 'server_info', + params: [] + }) + }) + + if (response.status !== 200) { + return new Error(response.statusText + ': ' + response.status) + } + + return true + } catch (error) { + return error as Error + } + } + + /** + * Check WS connection + * @param url - Websocket URL + * @returns Connection status + */ + async checkWsConnection(url?: string): Promise { + try { + const result: any = await checkWebSocket(url ?? this.network.wsUrl ?? '') + + if (result instanceof Error) { + return result + } + + return true + } catch (error) { + return error as Error + } + } + + /** + * Update network configuration of the provider + * @param network - Network configuration + */ + update(network: NetworkConfigInterface): void { + this.network = network + Provider._instance = this + this.explorer = network.testnet ? 'https://testnet.xrpl.org/' : 'https://livenet.xrpl.org/' + if (!network.rpcUrl) { + this.network.rpcUrl = network.testnet ? this.testnetRpc2 : this.mainnetRpc + } + if (!network.wsUrl) { + this.network.wsUrl = network.testnet ? this.testnetWs2 : this.mainnetWs + } + if (!this.network.wsUrl) { + throw new Error(ErrorTypeEnum.WS_URL_NOT_DEFINED) + } + if (!this.network.rpcUrl) { + throw new Error('RPC URL is not defined') + } + this.ws = new WsClient(this.network.wsUrl) + this.rpc = new Client(this.network.rpcUrl) + } + + /** + * Get the current network configuration is testnet or not + * @returns Testnet or not + */ + isTestnet(): boolean { + return this.network?.testnet ?? false + } + + /** + * Create memo object + * @param memo - Memo data + * @returns Memo object + */ + createMemo(memo: string): Memo { + return { + Memo: { + MemoData: Buffer.from(memo).toString('hex'), + MemoType: Buffer.from('text').toString('hex') + } + } + } +} diff --git a/packages/networks/xrpl/src/services/TransactionListener.ts b/packages/networks/xrpl/src/services/TransactionListener.ts new file mode 100644 index 0000000..e9f41db --- /dev/null +++ b/packages/networks/xrpl/src/services/TransactionListener.ts @@ -0,0 +1,321 @@ +import type { + TransactionTypeEnum, + DynamicTransactionType, + TransactionListenerInterface, + DynamicTransactionListenerFilterType, + TransactionId +} from '@multiplechain/types' +import { Provider } from './Provider' +import { objectsEqual } from '@multiplechain/utils' +import { Transaction } from '../models/Transaction' +import { CoinTransaction } from '../models/CoinTransaction' +import { TransactionListenerProcessIndex } from '@multiplechain/types' +import { + type SubscribeRequest, + type TransactionStream, + type UnsubscribeRequest, + type Client as WsClient +} from 'xrpl' + +type Command = Omit + +type TransactionStreamWithHash = TransactionStream & { hash: string } + +type TransactionListenerTriggerType = DynamicTransactionType< + T, + Transaction, + Transaction, + CoinTransaction, + Transaction, + Transaction +> + +type TransactionListenerCallbackType< + T extends TransactionTypeEnum, + Transaction = TransactionListenerTriggerType +> = (transaction: Transaction) => void + +export class TransactionListener< + T extends TransactionTypeEnum, + DTransaction extends TransactionListenerTriggerType, + CallBackType extends TransactionListenerCallbackType +> implements TransactionListenerInterface +{ + /** + * Transaction type + */ + type: T + + /** + * Provider + */ + provider: Provider + + /** + * Listener status + */ + status: boolean = false + + /** + * Transaction listener callback + */ + callbacks: CallBackType[] = [] + + /** + * Triggered transactions + */ + triggeredTransactions: TransactionId[] = [] + + /** + * Transaction listener filter + */ + filter?: DynamicTransactionListenerFilterType | Record + + /** + * WebSocket + */ + webSocket: WsClient + + /** + * Dynamic stop method + */ + dynamicStop: () => void = () => {} + + /** + * @param type - Transaction type + * @param filter - Transaction listener filter + * @param provider - Provider + */ + constructor(type: T, filter?: DynamicTransactionListenerFilterType, provider?: Provider) { + this.type = type + this.filter = filter ?? {} + this.provider = provider ?? Provider.instance + } + + /** + * Close the listener + */ + stop(): void { + if (this.status) { + this.status = false + this.dynamicStop() + } + } + + /** + * Start the listener + */ + start(): void { + if (!this.status) { + this.status = true + // @ts-expect-error allow dynamic access + this[TransactionListenerProcessIndex[this.type]]() + } + } + + /** + * Get the listener status + * @returns Listener status + */ + getStatus(): boolean { + return this.status + } + + /** + * Listen to the transaction events + * @param callback - Transaction listener callback + * @returns Connection status + */ + async on(callback: CallBackType): Promise { + if (this.webSocket === undefined) { + try { + await this.provider.ws.connect() + this.webSocket = this.provider.ws + } catch (error) { + throw new Error( + 'WebSocket connection is not available' + + (error instanceof Error ? ': ' + error.message : '') + ) + } + } + + this.start() + this.callbacks.push(callback) + + return true + } + + /** + * Trigger the event when a transaction is detected + * @param transaction - Transaction data + */ + trigger(transaction: TransactionListenerTriggerType): void { + if (!this.triggeredTransactions.includes(transaction.id)) { + this.triggeredTransactions.push(transaction.id) + this.callbacks.forEach((callback) => { + callback(transaction as unknown as DTransaction) + }) + } + } + + idByFilter(): string { + return btoa(JSON.stringify(this.filter) + this.type) + } + + createCommands(args: Command): { + sub: SubscribeRequest + unSub: UnsubscribeRequest + } { + return { + sub: { + id: this.idByFilter(), + command: 'subscribe', + ...args + }, + unSub: { + id: this.idByFilter(), + command: 'unsubscribe', + ...args + } + } + } + + /** + * General transaction process + */ + generalProcess(): void { + const args: Command = { streams: ['transactions'] } + + if (this.filter?.signer) { + args.accounts = [this.filter.signer] + } + + const { sub, unSub } = this.createCommands(args) + + void this.webSocket.request(sub).then(() => { + const callback = (tx: TransactionStreamWithHash): void => { + if ( + this.filter?.signer !== undefined && + tx.tx_json?.Account.toLowerCase() !== this.filter.signer.toLowerCase() + ) { + return + } + + this.trigger(new Transaction(tx.hash)) + } + + this.webSocket.on('transaction', callback) + + this.dynamicStop = () => { + void this.webSocket.request(unSub) + void this.webSocket.off('transaction', callback) + } + }) + } + + /** + * Contract transaction process + */ + contractProcess(): void { + throw new Error('This method is not implemented for CRPl.') + } + + /** + * Coin transaction process + */ + coinProcess(): void { + const filter = this.filter as DynamicTransactionListenerFilterType + + if ( + filter.signer !== undefined && + filter.sender !== undefined && + filter.signer !== filter.sender + ) { + throw new Error( + 'Sender and signer must be the same in coin transactions. Or only one of them can be defined.' + ) + } + + const sender = filter.sender ?? filter.signer + + const args: Command & { + accounts: string[] + } = { streams: ['transactions'], accounts: [] } + + if (sender) { + args.accounts.push(sender) + } + + if (filter.receiver) { + args.accounts.push(filter.receiver) + } + + const { sub, unSub } = this.createCommands(args) + + void this.webSocket.request(sub).then(() => { + const callback = async ( + tx: TransactionStreamWithHash & { + tx_json: { + Account: string + Destination?: string + } + } + ): Promise => { + interface ParamsType { + sender?: string + receiver?: string + } + + const expectedParams: ParamsType = {} + const receivedParams: ParamsType = {} + + if (sender !== undefined) { + expectedParams.sender = sender.toLowerCase() + receivedParams.sender = tx.tx_json?.Account.toLowerCase() + } + + if (filter.receiver !== undefined) { + expectedParams.receiver = filter.receiver.toLowerCase() + receivedParams.receiver = tx.tx_json?.Destination?.toLowerCase() + } + + if (!objectsEqual(expectedParams, receivedParams)) { + return + } + + const transaction = new CoinTransaction(tx.hash) + + if (filter.amount !== undefined) { + await transaction.wait() + const amount = await transaction.getAmount() + if (amount !== filter.amount) { + return + } + } + + this.trigger(transaction) + } + + this.webSocket.on('transaction', callback) + + this.dynamicStop = () => { + void this.webSocket.request(unSub) + void this.webSocket.off('transaction', callback) + } + }) + } + + /** + * Token transaction process + */ + tokenProcess(): void { + throw new Error('This method is not implemented for CRPl.') + } + + /** + * NFT transaction process + */ + nftProcess(): void { + throw new Error('This method is not implemented for CRPl.') + } +} diff --git a/packages/networks/xrpl/src/services/TransactionSigner.ts b/packages/networks/xrpl/src/services/TransactionSigner.ts new file mode 100644 index 0000000..ecbbcaf --- /dev/null +++ b/packages/networks/xrpl/src/services/TransactionSigner.ts @@ -0,0 +1,97 @@ +import { Provider } from '../services/Provider' +import { type ECDSA, Wallet, type SubmittableTransaction } from 'xrpl' +import type { PrivateKey, TransactionId, TransactionSignerInterface } from '@multiplechain/types' + +export interface SignedTransaction { + tx_blob: string + hash: string +} + +export type RawTransaction = SubmittableTransaction & { + Destination?: string + Amount?: string +} + +export class TransactionSigner + implements TransactionSignerInterface +{ + /** + * Transaction data from the blockchain network + */ + rawData: RawTransaction + + /** + * Signed transaction data + */ + signedData?: SignedTransaction + + /** + * Blockchain network provider + */ + provider: Provider + + /** + * @param rawData - Transaction data + * @param provider - Blockchain network provider + */ + constructor(rawData: RawTransaction, provider?: Provider) { + this.rawData = rawData + this.provider = provider ?? Provider.instance + } + + /** + * Sign the transaction + * @param privateKey - Transaction data + * @param algorithm - Blockchain network provider + * @returns Signed transaction data + */ + async sign(privateKey: PrivateKey, algorithm?: ECDSA): Promise { + await this.provider.ws.connect() + + const senderWallet = Wallet.fromSeed(privateKey, { + algorithm + }) + + this.rawData = await this.provider.ws.autofill(this.rawData) + + this.signedData = senderWallet.sign(this.rawData) + + return this + } + + /** + * Send the transaction to the blockchain network + * @returns Transaction ID + */ + async send(): Promise { + if (!this.signedData) { + throw new Error('Transaction not signed') + } + + const { result } = await this.provider.ws.submit(this.signedData.tx_blob) + + if (result.engine_result !== 'tesSUCCESS') { + throw new Error(`Transaction failed: ${result.engine_result_message}`) + } + + await this.provider.ws.disconnect() + + return this.signedData.hash + } + + /** + * Get the raw transaction data + * @returns Transaction data + */ + getRawData(): RawTransaction { + return this.rawData + } + + /** + * Get the signed transaction data + * @returns Signed transaction data + */ + getSignedData(): SignedTransaction { + return this.signedData ?? { tx_blob: '', hash: '' } + } +} diff --git a/packages/networks/xrpl/src/services/index.ts b/packages/networks/xrpl/src/services/index.ts new file mode 100644 index 0000000..0b05d2e --- /dev/null +++ b/packages/networks/xrpl/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './TransactionSigner' +export * from './TransactionListener' diff --git a/packages/networks/xrpl/tests/assets.spec.ts b/packages/networks/xrpl/tests/assets.spec.ts new file mode 100644 index 0000000..b769acf --- /dev/null +++ b/packages/networks/xrpl/tests/assets.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, assert } from 'vitest' + +import { Coin } from '../src/assets/Coin' +import { math } from '@multiplechain/utils' +import { Transaction } from '../src/models/Transaction' +import { TransactionStatusEnum, type TransactionId } from '@multiplechain/types' +import { TransactionSigner } from '../src/services/TransactionSigner' +import { ECDSA } from 'xrpl' + +const testAmount = Number(process.env.XRP_TRANSFER_AMOUNT) +const senderTestAddress = String(process.env.XRP_SENDER_ADDRESS) +const receiverTestAddress = String(process.env.XRP_RECEIVER_ADDRESS) +const senderPrivateKey = String(process.env.XRP_SENDER_PRIVATE_KEY) +const transferTestIsActive = Boolean(process.env.XRP_TRANSFER_TEST_IS_ACTIVE !== 'false') + +const checkSigner = async (signer: TransactionSigner, privateKey?: string): Promise => { + expect(signer).toBeInstanceOf(TransactionSigner) + + const rawData = signer.getRawData() + + assert.isObject(rawData) + + await signer.sign(privateKey ?? senderPrivateKey, ECDSA.secp256k1) + + assert.isObject(signer.getSignedData()) +} + +const checkTx = async (transactionId: TransactionId): Promise => { + const transaction = new Transaction(transactionId) + const status = await transaction.wait(10 * 1000) + expect(status).toBe(TransactionStatusEnum.CONFIRMED) +} + +describe('Coin', () => { + const coin = new Coin() + it('Name and symbol', () => { + expect(coin.getName()).toBe('XRP') + expect(coin.getSymbol()).toBe('XRP') + }) + + it('Decimals', () => { + expect(coin.getDecimals()).toBe(6) + }) + + it('Balance', async () => { + const balance = await coin.getBalance('rsDiH2LtPbcmkbTT3iLfKcPVtCJPGXxjry') + expect(balance).toBe(100) + }) + + it('Transfer', async () => { + const signer = await coin.transfer(senderTestAddress, receiverTestAddress, testAmount) + + await checkSigner(signer) + + if (!transferTestIsActive) return + + const beforeBalance = await coin.getBalance(receiverTestAddress) + + await checkTx(await signer.send()) + + const afterBalance = await coin.getBalance(receiverTestAddress) + expect(afterBalance).toBe(math.add(beforeBalance, testAmount)) + }) +}) diff --git a/packages/networks/xrpl/tests/models.spec.ts b/packages/networks/xrpl/tests/models.spec.ts new file mode 100644 index 0000000..8927fb6 --- /dev/null +++ b/packages/networks/xrpl/tests/models.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest' +import { Transaction } from '../src/models/Transaction' +import { CoinTransaction } from '../src/models/CoinTransaction' +import { AssetDirectionEnum, TransactionStatusEnum } from '@multiplechain/types' + +const testAmount = 0.001 +const senderTestAddress = String(process.env.XRP_SENDER_ADDRESS) +const receiverTestAddress = String(process.env.XRP_RECEIVER_ADDRESS) +const txId = '8F0EFC4482865B47B156F1D001B4EAB3DC0B0C7231AE99377B634B1E3FA1563B' + +describe('Transaction', () => { + const tx = new Transaction(txId) + it('Id', async () => { + expect(tx.getId()).toBe(txId) + }) + + it('Data', async () => { + expect(await tx.getData()).toBeTypeOf('object') + }) + + it('Wait', async () => { + expect(await tx.wait()).toBe(TransactionStatusEnum.CONFIRMED) + }) + + it('URL', async () => { + expect(tx.getUrl()).toBe('https://testnet.xrpl.org/transactions/' + txId) + }) + + it('Sender', async () => { + expect((await tx.getSigner()).toLowerCase()).toBe(senderTestAddress.toLowerCase()) + }) + + it('Fee', async () => { + expect(await tx.getFee()).toBe(0.000012) + }) + + it('Block Number', async () => { + expect(await tx.getBlockNumber()).toBe(4436513) + }) + + it('Block Timestamp', async () => { + expect(await tx.getBlockTimestamp()).toBe(791630821) + }) + + it('Block Confirmation Count', async () => { + expect(await tx.getBlockConfirmationCount()).toBeGreaterThan(13) + }) + + it('Status', async () => { + expect(await tx.getStatus()).toBe(TransactionStatusEnum.CONFIRMED) + }) +}) + +describe('Coin Transaction', () => { + const tx = new CoinTransaction(txId) + + it('Receiver', async () => { + expect((await tx.getReceiver()).toLowerCase()).toBe(receiverTestAddress.toLowerCase()) + }) + + it('Amount', async () => { + expect(await tx.getAmount()).toBe(testAmount) + }) + + it('Verify Transfer', async () => { + expect( + await tx.verifyTransfer(AssetDirectionEnum.INCOMING, receiverTestAddress, testAmount) + ).toBe(TransactionStatusEnum.CONFIRMED) + + expect( + await tx.verifyTransfer(AssetDirectionEnum.OUTGOING, senderTestAddress, testAmount) + ).toBe(TransactionStatusEnum.CONFIRMED) + + expect( + await tx.verifyTransfer(AssetDirectionEnum.OUTGOING, receiverTestAddress, testAmount) + ).toBe(TransactionStatusEnum.FAILED) + }) +}) diff --git a/packages/networks/xrpl/tests/services.spec.ts b/packages/networks/xrpl/tests/services.spec.ts new file mode 100644 index 0000000..e0fddaf --- /dev/null +++ b/packages/networks/xrpl/tests/services.spec.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest' + +import { provider } from './setup' +import { Provider } from '../src/services/Provider' +import { TransactionListener } from '../src/services/TransactionListener' +import { TransactionTypeEnum } from '@multiplechain/types' +import { CoinTransaction } from '../src/models/CoinTransaction' +import { Coin } from '../src/assets/Coin' +import { sleep } from '@multiplechain/utils' + +const senderTestAddress = String(process.env.XRP_SENDER_ADDRESS) +const receiverTestAddress = String(process.env.XRP_RECEIVER_ADDRESS) +const senderPrivateKey = String(process.env.XRP_SENDER_PRIVATE_KEY) +const listenerTestIsActive = Boolean(process.env.XRP_LISTENER_TEST_IS_ACTIVE !== 'false') + +describe('Provider', () => { + it('isTestnet', () => { + expect(provider.isTestnet()).toBe(true) + }) + + it('instance', () => { + expect(Provider.instance).toBe(provider) + }) + + it('checkRpcConnection', async () => { + expect(await provider.checkRpcConnection()).toBe(true) + }) + + it('checkWsConnection', async () => { + expect(await provider.checkWsConnection()).toBe(true) + }) +}) + +describe('Transaction Listener', () => { + if (!listenerTestIsActive) { + it('No test is active', () => { + expect(true).toBe(true) + }) + return + } + + it('Coin', async () => { + const listener = new TransactionListener(TransactionTypeEnum.COIN, { + signer: senderTestAddress, + receiver: receiverTestAddress + }) + + const signer = await new Coin().transfer(senderTestAddress, receiverTestAddress, 0.0001) + + const waitListenerEvent = async (): Promise => { + return await new Promise((resolve, reject) => { + void listener + .on((transaction) => { + listener.stop() + resolve(transaction) + }) + .then(async () => { + await sleep(2000) + void (await signer.sign(senderPrivateKey)).send() + }) + .catch(reject) + }) + } + + expect(await waitListenerEvent()).toBeInstanceOf(CoinTransaction) + }) +}) diff --git a/packages/networks/xrpl/tests/setup.ts b/packages/networks/xrpl/tests/setup.ts new file mode 100644 index 0000000..476b875 --- /dev/null +++ b/packages/networks/xrpl/tests/setup.ts @@ -0,0 +1,13 @@ +import { Provider } from '../src/services/Provider' + +let provider: Provider + +try { + provider = Provider.instance +} catch (e) { + provider = new Provider({ + testnet: true + }) +} + +export { provider } diff --git a/packages/networks/xrpl/tsconfig.json b/packages/networks/xrpl/tsconfig.json new file mode 100644 index 0000000..594d173 --- /dev/null +++ b/packages/networks/xrpl/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "noEmit": true, + "composite": true, + "declaration": true, + "outDir": "./dist/esm", + "declarationDir": "./dist/types" + }, + "extends": "../../../tsconfig.json", + "include": [ + "src", + ".eslintrc.json", + "tests", + "vite.config.ts", + "esbuild.ts", + "vitest.config.ts", + "../../../esbuild.ts", + "../../../vite.config.ts", + "../../../vitest.config.ts" + ] +} diff --git a/packages/networks/xrpl/vite.config.ts b/packages/networks/xrpl/vite.config.ts new file mode 100644 index 0000000..7a87a00 --- /dev/null +++ b/packages/networks/xrpl/vite.config.ts @@ -0,0 +1,10 @@ +import { mergeConfig } from 'vite' +import mainConfig from '../../../vite.config' + +export default mergeConfig(mainConfig, { + build: { + lib: { + name: 'MultipleChain.XRPl' + } + } +}) diff --git a/packages/networks/xrpl/vite.svg b/packages/networks/xrpl/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/packages/networks/xrpl/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/networks/xrpl/vitest.config.ts b/packages/networks/xrpl/vitest.config.ts new file mode 100644 index 0000000..8a33630 --- /dev/null +++ b/packages/networks/xrpl/vitest.config.ts @@ -0,0 +1,12 @@ +import { mergeConfig, defineConfig } from 'vitest/config' +import mainConfig from '../../../vite.config' + +export default mergeConfig( + mainConfig, + defineConfig({ + test: { + testTimeout: 600000, + setupFiles: ['./tests/setup.ts'] + } + }) +) diff --git a/vitest.config.ts b/vitest.config.ts index 94f1351..4e21eec 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -32,6 +32,7 @@ export default mergeConfig( './packages/networks/bitcoin/tests/setup.ts', './packages/networks/solana/tests/setup.ts', './packages/networks/tron/tests/setup.ts', + './packages/networks/xrpl/tests/setup.ts', './packages/networks/ton/tests/setup.ts' ] }