diff --git a/.github/workflows/code-checks.yml b/.github/workflows/code-checks.yml index f34916c..61fb2f1 100644 --- a/.github/workflows/code-checks.yml +++ b/.github/workflows/code-checks.yml @@ -55,16 +55,6 @@ jobs: - name: Typecheck run: yarn lint:typecheck - lint-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Commit Message - uses: wagoid/commitlint-github-action@v4 - test-unit: needs: build runs-on: ubuntu-latest diff --git a/package.json b/package.json index 58c6ea0..3554cf3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.2.7", + "version": "2.4.9", "description": "This library provides a comprehensive set of interfaces and functions for minting dlcBTC tokens on supported blockchains.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -15,7 +15,8 @@ "./models": "./dist/models/index.js", "./bitcoin-functions": "./dist/functions/bitcoin/index.js", "./attestor-request-functions": "./dist/functions/attestor/index.js", - "./ethereum-functions": "./dist/functions/ethereum/index.js" + "./ethereum-functions": "./dist/functions/ethereum/index.js", + "./ripple-functions": "./dist/functions/ripple/index.js" }, "scripts": { "clean": "rm -rf dist && rm -rf node_modules", @@ -58,20 +59,24 @@ "typescript-eslint": "^7.7.0" }, "dependencies": { - "@ledgerhq/hw-app-btc": "^10.2.4", - "@noble/hashes": "^1.4.0", - "@scure/base": "^1.1.6", - "@scure/btc-signer": "^1.3.1", - "@types/ramda": "^0.30.1", - "bip32": "^4.0.0", - "bitcoinjs-lib": "^6.1.5", - "chalk": "^5.3.0", - "decimal.js": "^10.4.3", + "@gemwallet/api": "3.8.0", + "@ledgerhq/hw-app-btc": "10.4.1", + "@ledgerhq/hw-app-xrp": "6.29.4", + "@noble/hashes": "1.4.0", + "@scure/base": "1.1.8", + "@scure/btc-signer": "1.3.2", + "@types/ramda": "0.30.1", + "bip32": "4.0.0", + "bitcoinjs-lib": "6.1.6", + "chalk": "5.3.0", + "decimal.js": "10.4.3", "ethers": "5.7.2", - "ledger-bitcoin": "^0.2.3", - "prompts": "^2.4.2", - "ramda": "^0.30.1", - "scure": "^1.6.0", - "tiny-secp256k1": "^2.2.3" + "ledger-bitcoin": "0.2.3", + "prompts": "2.4.2", + "ramda": "0.30.1", + "ripple-binary-codec": "2.1.0", + "scure": "1.6.0", + "tiny-secp256k1": "2.2.3", + "xrpl": "4.0.0" } } diff --git a/src/constants/ripple.constants.ts b/src/constants/ripple.constants.ts new file mode 100644 index 0000000..a9fe18d --- /dev/null +++ b/src/constants/ripple.constants.ts @@ -0,0 +1,4 @@ +import { convertStringToHex } from 'xrpl'; + +export const TRANSACTION_SUCCESS_CODE = 'tesSUCCESS'; +export const XRPL_DLCBTC_CURRENCY_HEX = convertStringToHex('dlcBTC').padEnd(40, '0'); diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts index f840db7..963a53c 100644 --- a/src/dlc-handlers/software-wallet-dlc-handler.ts +++ b/src/dlc-handlers/software-wallet-dlc-handler.ts @@ -1,7 +1,6 @@ import { Transaction, p2tr, p2wpkh } from '@scure/btc-signer'; import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; import { Network } from 'bitcoinjs-lib'; -import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; import { createTaprootMultisigPayment, @@ -33,35 +32,11 @@ export class SoftwareWalletDLCHandler { taprootDerivedPublicKey: string, fundingPaymentType: 'wpkh' | 'tr', bitcoinNetwork: Network, - bitcoinBlockchainAPI?: string, - bitcoinBlockchainFeeRecommendationAPI?: string + bitcoinBlockchainAPI: string, + bitcoinBlockchainFeeRecommendationAPI: string ) { - switch (bitcoinNetwork) { - case bitcoin: - this.bitcoinBlockchainAPI = 'https://mempool.space/api'; - this.bitcoinBlockchainFeeRecommendationAPI = - 'https://mempool.space/api/v1/fees/recommended'; - break; - case testnet: - this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; - this.bitcoinBlockchainFeeRecommendationAPI = - 'https://mempool.space/testnet/api/v1/fees/recommended'; - break; - case regtest: - if ( - bitcoinBlockchainAPI === undefined || - bitcoinBlockchainFeeRecommendationAPI === undefined - ) { - throw new Error( - 'Regtest requires a Bitcoin Blockchain API and a Bitcoin Blockchain Fee Recommendation API' - ); - } - this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; - this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; - break; - default: - throw new Error('Invalid Bitcoin Network'); - } + this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; + this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; this.fundingPaymentType = fundingPaymentType; this.bitcoinNetwork = bitcoinNetwork; this.fundingDerivedPublicKey = fundingDerivedPublicKey; diff --git a/src/functions/attestor/attestor-request.functions.ts b/src/functions/attestor/attestor-request.functions.ts index 64992bc..f4026e0 100644 --- a/src/functions/attestor/attestor-request.functions.ts +++ b/src/functions/attestor/attestor-request.functions.ts @@ -1,11 +1,37 @@ import { equals, filter, isEmpty, isNotNil, join, map, prop } from 'ramda'; import { + AttestorChainID, FundingTXAttestorInfo, WithdrawDepositTXAttestorInfo, } from '../../models/attestor.models.js'; import { AttestorError } from '../../models/errors.js'; -import { sendRequest } from '../request/request.functions.js'; +import { sendGetRequest, sendRequest } from '../request/request.functions.js'; + +export async function submitSetupXRPLVaultRequest( + coordinatorURL: string, + userXRPLAddress: string, + attestorChainID: AttestorChainID +): Promise { + const requestBody = JSON.stringify({ + user_xrpl_address: userXRPLAddress, + chain: attestorChainID, + }); + return sendRequest(`${coordinatorURL}/app/setup-xrpl-vault`, requestBody); +} + +export async function submitXRPLCheckToCash( + coordinatorURL: string, + txHash: string, + attestorChainID: AttestorChainID +): Promise { + const requestBody = JSON.stringify({ tx_hash: txHash, chain: attestorChainID }); + return sendRequest(`${coordinatorURL}/app/cash-xrpl-check`, requestBody); +} + +export async function getAttestorExtendedGroupPublicKey(coordinatorURL: string): Promise { + return sendGetRequest(`${coordinatorURL}/tss/get-extended-group-publickey`); +} export async function submitFundingPSBT( attestorRootURLs: string[], diff --git a/src/functions/attestor/index.ts b/src/functions/attestor/index.ts index 8b032cd..88d8d90 100644 --- a/src/functions/attestor/index.ts +++ b/src/functions/attestor/index.ts @@ -1,4 +1,7 @@ export { submitFundingPSBT, submitWithdrawDepositPSBT, + getAttestorExtendedGroupPublicKey, + submitSetupXRPLVaultRequest, + submitXRPLCheckToCash, } from '../attestor/attestor-request.functions.js'; diff --git a/src/functions/bitcoin/bitcoin-request-functions.ts b/src/functions/bitcoin/bitcoin-request-functions.ts index 83f4240..33cdb09 100644 --- a/src/functions/bitcoin/bitcoin-request-functions.ts +++ b/src/functions/bitcoin/bitcoin-request-functions.ts @@ -17,7 +17,9 @@ export async function fetchBitcoinTransaction( const response = await fetch(bitcoinBlockchainAPITransactionEndpoint); if (!response.ok) - throw new Error(`Bitcoin Network Transaction Response was not OK: ${response.statusText}`); + throw new Error( + `Bitcoin Network Transaction Response was not OK: ${response.statusText} - btc tx id: ${txID}` + ); return await response.json(); } catch (error) { diff --git a/src/functions/request/request.functions.ts b/src/functions/request/request.functions.ts index 5955b3b..1f05374 100644 --- a/src/functions/request/request.functions.ts +++ b/src/functions/request/request.functions.ts @@ -6,6 +6,21 @@ export async function sendRequest(url: string, body: string): Promise { }); if (!response.ok) { - throw new Error(`Response ${url} was not OK: ${response.statusText}`); + const errorMessage = await response.text(); + throw new Error(`Request to ${url} failed: ${response.statusText} - ${errorMessage}`); } } + +export async function sendGetRequest(url: string): Promise { + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, + }); + + if (!response.ok) { + const errorMessage = await response.text(); + throw new Error(`Request to ${url} failed: ${response.statusText} - ${errorMessage}`); + } + + return response.text(); +} diff --git a/src/functions/ripple/index.ts b/src/functions/ripple/index.ts new file mode 100644 index 0000000..aac5936 --- /dev/null +++ b/src/functions/ripple/index.ts @@ -0,0 +1 @@ +export * from './ripple.functions.js'; diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts new file mode 100644 index 0000000..30ce9ea --- /dev/null +++ b/src/functions/ripple/ripple.functions.ts @@ -0,0 +1,406 @@ +import { Decimal } from 'decimal.js'; +import { BigNumber } from 'ethers'; +import { + AccountLinesRequest, + AccountLinesResponse, + AccountNFToken, + AccountNFTsRequest, + AccountObjectsRequest, + CheckCreate, + Client, + LedgerEntry, + SubmittableTransaction, + Transaction, + TransactionMetadataBase, + TrustSet, + TxResponse, + Wallet, + convertHexToString, + convertStringToHex, +} from 'xrpl'; + +import { + TRANSACTION_SUCCESS_CODE, + XRPL_DLCBTC_CURRENCY_HEX, +} from '../../constants/ripple.constants.js'; +import { RippleError } from '../../models/errors.js'; +import { RawVault } from '../../models/ethereum-models.js'; +import { SignResponse } from '../../models/ripple.model.js'; +import { shiftValue, unshiftValue } from '../../utilities/index.js'; + +function hexFieldsToLowercase(vault: RawVault): RawVault { + return { + ...vault, + uuid: vault.uuid.toLowerCase(), + fundingTxId: vault.fundingTxId.toLowerCase(), + wdTxId: vault.wdTxId.toLowerCase(), + btcFeeRecipient: vault.btcFeeRecipient.toLowerCase(), + taprootPubKey: vault.taprootPubKey.toLowerCase(), + }; +} + +export function encodeURI(vault: RawVault): string { + const version = parseInt('1', 16).toString().padStart(2, '0'); // 1 as hex + const status = parseInt(vault.status.toString(), 16).toString().padStart(2, '0'); + let uuid = vault.uuid; + if (uuid === '') { + uuid = vault.uuid.padStart(64, '0'); + } + if (uuid.substring(0, 2) === '0x') { + uuid = uuid.slice(2); + } + const valueLockedPadded = vault.valueLocked._hex.substring(2).padStart(16, '0'); + const valueMintedPadded = vault.valueMinted._hex.substring(2).padStart(16, '0'); + const timestamp = vault.timestamp._hex.substring(2).padStart(20, '0'); + const creator = convertStringToHex(vault.creator).padStart(68, '0'); + const fundingTxId = vault.fundingTxId.padStart(64, '0'); + const wdTxId = vault.wdTxId.padStart(64, '0'); + const btcMintFeeBasisPoints = vault.btcMintFeeBasisPoints._hex.substring(2).padStart(8, '0'); + const btcRedeemFeeBasisPoints = vault.btcRedeemFeeBasisPoints._hex.substring(2).padStart(8, '0'); + const btcFeeRecipient = vault.btcFeeRecipient.padStart(66, '0'); + const taprootPubKey = vault.taprootPubKey.padStart(64, '0'); + + const nftURI = [ + version, + status, + uuid, + valueLockedPadded, + valueMintedPadded, + timestamp, + creator, + fundingTxId, + wdTxId, + btcMintFeeBasisPoints, + btcRedeemFeeBasisPoints, + btcFeeRecipient, + taprootPubKey, + ].join(''); + + console.log('Encoded URI:', nftURI); + + return nftURI; +} + +export function decodeURI(URI: string): RawVault { + try { + return { + status: parseInt(URI.slice(2, 4), 16), + uuid: `0x${URI.slice(4, 68)}`, + valueLocked: BigNumber.from(`0x${URI.slice(68, 84)}`), + valueMinted: BigNumber.from(`0x${URI.slice(84, 100)}`), + protocolContract: convertHexToString(URI.slice(120, 188)), + timestamp: BigNumber.from(`0x${URI.slice(100, 120)}`), + creator: convertHexToString(URI.slice(120, 188)), + fundingTxId: URI.slice(188, 252) === '0'.repeat(64) ? '' : URI.slice(188, 252), + wdTxId: URI.slice(252, 316) === '0'.repeat(64) ? '' : URI.slice(252, 316), + btcMintFeeBasisPoints: BigNumber.from(`0x${URI.slice(316, 324)}`), + btcRedeemFeeBasisPoints: BigNumber.from(`0x${URI.slice(324, 332)}`), + btcFeeRecipient: URI.slice(332, 398), + taprootPubKey: URI.slice(398, 462), + closingTxId: '', // Deprecated + }; + } catch (error) { + throw new Error(`Could not decode NFT URI: ${error}`); + } +} + +export function checkRippleTransactionResult(txResponse: TxResponse): void { + const meta = txResponse.result.meta; + + if (!meta) { + throw new RippleError('Transaction Metadata not found'); + } + + if (typeof meta === 'string') { + throw new RippleError(`Could not read Transaction Result of: ${meta}`); + } + + const transactionResult = (meta as TransactionMetadataBase).TransactionResult; + + if (transactionResult !== TRANSACTION_SUCCESS_CODE) { + throw new RippleError(`Transaction failed: ${transactionResult}`); + } +} + +export function getRippleClient(serverEndpoint: string): Client { + return new Client(serverEndpoint); +} + +export function getRippleWallet(seedPhrase: string): Wallet { + return Wallet.fromSeed(seedPhrase); +} + +export async function connectRippleClient(rippleClient: Client): Promise { + if (rippleClient.isConnected()) { + return false; + } + await rippleClient.connect(); + return true; +} + +export function formatRippleVaultUUID(vaultUUID: string): string { + return vaultUUID.startsWith('0x') ? vaultUUID.slice(2).toUpperCase() : vaultUUID.toUpperCase(); +} + +export function findNFTByUUID(rippleNFTs: AccountNFToken[], vaultUUID: string): AccountNFToken { + const rippleNFT = rippleNFTs.find( + rippleNFT => rippleNFT.URI && decodeURI(rippleNFT.URI).uuid.slice(2) === vaultUUID + ); + + if (!rippleNFT) { + throw new Error(`Could not find NFT with UUID: ${vaultUUID}`); + } + + return rippleNFT; +} + +export async function setTrustLine( + rippleClient: Client, + ownerAddress: string, + issuerAddress: string +): Promise { + await connectRippleClient(rippleClient); + + const accountNonXRPBalancesRequest: AccountLinesRequest = { + command: 'account_lines', + account: ownerAddress, + ledger_index: 'validated', + }; + + const { + result: { lines }, + }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); + + if ( + lines.some(line => line.currency === XRPL_DLCBTC_CURRENCY_HEX && line.account === issuerAddress) + ) { + console.log(`Trust Line already exists for Issuer: ${issuerAddress}`); + return; + } + + const trustSetTransactionRequest: TrustSet = { + TransactionType: 'TrustSet', + Account: ownerAddress, + LimitAmount: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + issuer: issuerAddress, + value: '10000000000', + }, + }; + + const updatedTrustSetTransactionRequest = await rippleClient.autofill(trustSetTransactionRequest); + return updatedTrustSetTransactionRequest; +} + +export async function getRippleVault( + rippleClient: Client, + issuerAddress: string, + vaultUUID: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID : `0x${vaultUUID}`; + + const allVaults = await getAllRippleVaults(rippleClient, issuerAddress); + + const matchingVaults = allVaults.filter( + vault => vault.uuid.toLowerCase() === formattedUUID.toLowerCase() + ); + + if (matchingVaults.length === 0) { + throw new RippleError(`Vault with UUID: ${formattedUUID} not found`); + } + + if (matchingVaults.length > 1) { + throw new RippleError(`Multiple Vaults found with UUID: ${formattedUUID}`); + } + + return matchingVaults[0]; + } catch (error) { + throw new RippleError(`Error getting Vault ${vaultUUID}: ${error}`); + } +} + +export async function getAllRippleVaults( + rippleClient: Client, + issuerAddress: string, + ownerAddress?: string +): Promise { + try { + await connectRippleClient(rippleClient); + + let marker: any = undefined; + const limit = 100; + let allRippleNFTs: any[] = []; + + do { + const getAccountNFTsRequest: AccountNFTsRequest = { + command: 'account_nfts', + account: issuerAddress, + limit, + marker, + }; + + const { + result: { account_nfts: rippleNFTs, marker: newMarker }, + } = await rippleClient.request(getAccountNFTsRequest); + + allRippleNFTs = allRippleNFTs.concat(rippleNFTs); + + marker = newMarker; + } while (marker); + + const rippleVaults = allRippleNFTs.map(nft => hexFieldsToLowercase(decodeURI(nft.URI!))); + + if (ownerAddress) { + return rippleVaults.filter(vault => vault.creator === ownerAddress); + } else { + return rippleVaults; + } + } catch (error) { + throw new RippleError(`Error getting Vaults: ${error}`); + } +} + +export async function signAndSubmitRippleTransaction( + rippleClient: Client, + rippleWallet: Wallet, + transaction: Transaction +): Promise> { + try { + const signResponse: SignResponse = rippleWallet.sign(transaction); + + const submitResponse: TxResponse = await rippleClient.submitAndWait( + signResponse.tx_blob + ); + + console.log(`Response for submitted Transaction Request:`, submitResponse); + + checkRippleTransactionResult(submitResponse); + + return submitResponse; + } catch (error) { + throw new RippleError(`Error signing and submitt Transaction: ${error}`); + } +} + +export async function getLockedBTCBalance( + rippleClient: Client, + userAddress: string, + issuerAddress: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const rippleVaults = await getAllRippleVaults(rippleClient, issuerAddress, userAddress); + + const lockedBTCBalance = rippleVaults.reduce((accumulator, vault) => { + return accumulator + vault.valueLocked.toNumber(); + }, 0); + + return lockedBTCBalance; + } catch (error) { + throw new RippleError(`Error getting locked BTC balance: ${error}`); + } +} + +export async function getDLCBTCBalance( + rippleClient: Client, + userAddress: string, + issuerAddress: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const accountNonXRPBalancesRequest: AccountLinesRequest = { + command: 'account_lines', + account: userAddress, + ledger_index: 'validated', + }; + + const { + result: { lines }, + }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); + + const dlcBTCBalance = lines.find( + line => line.currency === XRPL_DLCBTC_CURRENCY_HEX && line.account === issuerAddress + ); + if (!dlcBTCBalance) { + return 0; + } else { + return shiftValue(new Decimal(dlcBTCBalance.balance).toNumber()); + } + } catch (error) { + throw new RippleError(`Error getting BTC balance: ${error}`); + } +} + +export async function createCheck( + rippleClient: Client, + ownerAddress: string, + destinationAddress: string, + destinationTag: number = 1, + dlcBTCAmount: string, + vaultUUID: string +): Promise { + try { + await connectRippleClient(rippleClient); + + console.log(`Creating Check for Vault ${vaultUUID} with an amount of ${dlcBTCAmount}`); + + const amountAsNumber = new Decimal(dlcBTCAmount).toNumber(); + const shiftedAmountAsNumber = unshiftValue(amountAsNumber); + + const createCheckRequestJSON: CheckCreate = { + TransactionType: 'CheckCreate', + Account: ownerAddress, + Destination: destinationAddress, + DestinationTag: destinationTag, + SendMax: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + value: shiftedAmountAsNumber.toString(), + issuer: destinationAddress, + }, + InvoiceID: vaultUUID, + }; + + const updatedCreateCheckRequestJSON: CheckCreate = + await rippleClient.autofill(createCheckRequestJSON); + + return updatedCreateCheckRequestJSON; + } catch (error) { + throw new RippleError(`Error creating Check for Vault ${vaultUUID}: ${error}`); + } +} + +export async function getCheckByTXHash( + rippleClient: Client, + issuerAddress: string, + txHash: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const getAccountObjectsRequest: AccountObjectsRequest = { + command: 'account_objects', + account: issuerAddress, + ledger_index: 'validated', + type: 'check', + }; + + const { + result: { account_objects }, + } = await rippleClient.request(getAccountObjectsRequest); + + const check = account_objects.find(accountObject => accountObject.PreviousTxnID === txHash); + + if (!check) { + throw new RippleError(`Check with TX Hash: ${txHash} not found`); + } + + return check as LedgerEntry.Check; + } catch (error) { + throw new RippleError(`Error getting Check by TX Hash: ${error}`); + } +} diff --git a/src/index.ts b/src/index.ts index e750e7c..4a268d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,18 @@ import { LedgerDLCHandler } from './dlc-handlers/ledger-dlc-handler.js'; import { PrivateKeyDLCHandler } from './dlc-handlers/private-key-dlc-handler.js'; import { SoftwareWalletDLCHandler } from './dlc-handlers/software-wallet-dlc-handler.js'; import { EthereumHandler } from './network-handlers/ethereum-handler.js'; +import { RippleHandler } from './network-handlers/ripple-handler.js'; +import { GemXRPHandler } from './network-handlers/xrp-gem-wallet-handler.js'; +import { LedgerXRPHandler } from './network-handlers/xrp-ledger-handler.js'; import { ProofOfReserveHandler } from './proof-of-reserve-handlers/proof-of-reserve-handler.js'; export { PrivateKeyDLCHandler, LedgerDLCHandler, SoftwareWalletDLCHandler, + LedgerXRPHandler, + GemXRPHandler, EthereumHandler, ProofOfReserveHandler, + RippleHandler, }; diff --git a/src/models/attestor.models.ts b/src/models/attestor.models.ts index 92b9bd2..4a5e247 100644 --- a/src/models/attestor.models.ts +++ b/src/models/attestor.models.ts @@ -1,9 +1,20 @@ export type AttestorChainID = + | 'evm-mainnet' + | 'evm-sepolia' | 'evm-arbitrum' | 'evm-arbsepolia' + | 'evm-base' + | 'evm-basesepolia' + | 'evm-optimism' + | 'evm-opsepolia' + | 'evm-polygon' + | 'evm-polygonsepolia' | 'evm-localhost' | 'evm-hardhat-arb' - | 'evm-hardhat-eth'; + | 'evm-hardhat-eth' + | 'ripple-xrpl-mainnet' + | 'ripple-xrpl-testnet' + | 'ripple-xrpl-devnet'; export interface FundingTXAttestorInfo { vaultUUID: string; diff --git a/src/models/errors.ts b/src/models/errors.ts index 30e65d7..c5b440a 100644 --- a/src/models/errors.ts +++ b/src/models/errors.ts @@ -32,3 +32,10 @@ export class LedgerError extends Error { this.name = 'LedgerError'; } } + +export class RippleError extends Error { + constructor(message: string) { + super(message); + this.name = 'RippleError'; + } +} diff --git a/src/models/ethereum-models.ts b/src/models/ethereum-models.ts index 557d74f..fc64a1c 100644 --- a/src/models/ethereum-models.ts +++ b/src/models/ethereum-models.ts @@ -42,6 +42,20 @@ export interface RawVault { taprootPubKey: string; } +export interface SSPVaultUpdate { + status: number; + wdTxId: string; + taprootPubKey: string; +} + +export interface SSFVaultUpdate { + status: number; + fundingTxId: string; + wdTxId: string; + valueMinted: bigint; + valueLocked: bigint; +} + interface EthereumContract { name: string; address: string; diff --git a/src/models/index.ts b/src/models/index.ts index 40e320b..1f5c0df 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,7 +1,9 @@ export { Network } from 'bitcoinjs-lib/src/networks.js'; export { Transaction } from '@scure/btc-signer'; export { TransactionInputUpdate } from '@scure/btc-signer/psbt'; +export { Client, Wallet } from 'xrpl'; export * from '../models/bitcoin-models.js'; export * from '../models/errors.js'; export * from '../models/ethereum-models.js'; export * from '../models/attestor.models.js'; +export * from '../models/ripple.model.js'; diff --git a/src/models/ripple.model.ts b/src/models/ripple.model.ts new file mode 100644 index 0000000..63d3303 --- /dev/null +++ b/src/models/ripple.model.ts @@ -0,0 +1,4 @@ +export interface SignResponse { + tx_blob: string; + hash: string; +} diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts new file mode 100644 index 0000000..fc0d472 --- /dev/null +++ b/src/network-handlers/ripple-handler.ts @@ -0,0 +1,644 @@ +import { Decimal } from 'decimal.js'; +import { BigNumber } from 'ethers'; +import xrpl, { + AccountNFTsRequest, + AccountObject, + AccountObjectsResponse, + CheckCash, + IssuedCurrencyAmount, + Payment, + Request, + SubmittableTransaction, +} from 'xrpl'; +import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; + +import { XRPL_DLCBTC_CURRENCY_HEX } from '../constants/ripple.constants.js'; +import { + connectRippleClient, + decodeURI, + encodeURI, + getAllRippleVaults, + getCheckByTXHash, + getRippleVault, +} from '../functions/ripple/ripple.functions.js'; +import { RippleError } from '../models/errors.js'; +import { RawVault, SSFVaultUpdate, SSPVaultUpdate } from '../models/ethereum-models.js'; +import { shiftValue, unshiftValue } from '../utilities/index.js'; + +interface SignResponse { + tx_blob: string; + hash: string; +} + +function buildDefaultNftVault(): RawVault { + return { + uuid: `0x${'0'.repeat(64)}`, + valueLocked: BigNumber.from(0), + valueMinted: BigNumber.from(0), + protocolContract: '', + timestamp: BigNumber.from(0), + creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', + status: 0, + fundingTxId: '0'.repeat(64), + closingTxId: '', + wdTxId: '0'.repeat(64), + btcFeeRecipient: '03c9fc819e3c26ec4a58639add07f6372e810513f5d3d7374c25c65fdf1aefe4c5', + btcMintFeeBasisPoints: BigNumber.from(100), + btcRedeemFeeBasisPoints: BigNumber.from(100), + taprootPubKey: '0'.repeat(64), + }; +} + +export class RippleHandler { + private client: xrpl.Client; + private wallet: xrpl.Wallet; + private issuerAddress: string; + private minSigners: number; + + private constructor( + seedPhrase: string, + issuerAddress: string, + websocketURL: string, + minSigners: number + ) { + this.client = new xrpl.Client(websocketURL, { timeout: 10000 }); + this.wallet = xrpl.Wallet.fromSeed(seedPhrase); + this.issuerAddress = issuerAddress; + this.minSigners = minSigners; + } + + static fromSeed( + seedPhrase: string, + issuerAddress: string, + websocketURL: string, + minSigners: number + ): RippleHandler { + return new RippleHandler(seedPhrase, issuerAddress, websocketURL, minSigners); + } + + async withConnectionMgmt(callback: () => Promise): Promise { + console.log('Connecting to the async service...'); + const newConnection = !this.client.isConnected(); + try { + await connectRippleClient(this.client); + console.log('calling the callback service...'); + const result = await callback(); + return result; + } finally { + console.log('Disconnecting from the async service...'); + if (newConnection) { + // only disconnect if we connected in this function, otherwise leave the connection open + // This is to prevent closing a connection from an internally used function when the connection is still needed by the caller + // For example, getSigUpdateVaultForSSP calls getRawVault internally, and both need the connection, so we can't close the connection when getRawVault finishes + await this.client.disconnect(); + } + } + } + + async submit(signatures: string[]): Promise { + return await this.withConnectionMgmt(async () => { + try { + const multisig_tx = xrpl.multisign(signatures); + + const tx: xrpl.TxResponse = + await this.client.submitAndWait(multisig_tx); + const meta: NFTokenMintMetadata = tx.result.meta! as NFTokenMintMetadata; + + if (meta.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not burn temporary Ripple Vault: ${meta!.TransactionResult}` + ); + } + return tx.result.hash; + } catch (error) { + throw new RippleError(`Could not submit transaction: ${error}`); + } + }); + } + + async getNetworkInfo(): Promise { + return await this.withConnectionMgmt(async () => { + try { + return await this.client.request({ command: 'server_info' }); + } catch (error) { + throw new RippleError(`Could not fetch Network Info: ${error}`); + } + }); + } + + async getAddress(): Promise { + try { + return this.wallet.classicAddress; + } catch (error) { + throw new RippleError(`Could not fetch Address Info: ${error}`); + } + } + + async getRawVault(uuid: string): Promise { + return await this.withConnectionMgmt(async () => { + try { + return await getRippleVault(this.client, this.issuerAddress, uuid); + } catch (error) { + throw new RippleError(`Could not fetch Vault: ${error}`); + } + }); + } + + async setupVault( + uuid: string, + userAddress: string, + timeStamp: number, + btcMintFeeBasisPoints: number, + btcRedeemFeeBasisPoints: number + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + const newVault = buildDefaultNftVault(); + newVault.uuid = uuid; + newVault.creator = userAddress; + newVault.timestamp = BigNumber.from(timeStamp); + newVault.btcMintFeeBasisPoints = BigNumber.from(btcMintFeeBasisPoints); + newVault.btcRedeemFeeBasisPoints = BigNumber.from(btcRedeemFeeBasisPoints); + return await this.mintNFT(newVault); + } catch (error) { + throw new RippleError(`Could not setup Ripple Vault: ${error}`); + } + }); + } + + async withdraw(uuid: string, withdrawAmount: bigint): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log(`Performing Withdraw for User: ${uuid}`); + let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; + nftUUID = nftUUID.toUpperCase(); + const thisVault = await this.getRawVault(nftUUID); + const burnSig = await this.burnNFT(nftUUID, 1); + + thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount)); + const mintSig = await this.mintNFT(thisVault, 2); + return [burnSig, mintSig]; + } catch (error) { + throw new RippleError(`Unable to perform Withdraw for User: ${error}`); + } + }); + } + + async setVaultStatusFunded( + burnNFTSignedTxBlobs: string[], + mintTokensSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log('Doing the burn for SSF'); + const burn_multisig_tx = xrpl.multisign(burnNFTSignedTxBlobs); + const burnTx: xrpl.TxResponse = + await this.client.submitAndWait(burn_multisig_tx); + const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; + if (burnMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not burn temporary Ripple Vault: ${burnMeta!.TransactionResult}` + ); + } + + // multisig mint + if (mintTokensSignedTxBlobs.every(sig => sig !== '')) { + console.log('Success! Now minting the actual tokens!! How fun $$'); + + const mint_token_multisig_tx = xrpl.multisign(mintTokensSignedTxBlobs); + const mintTokenTx: xrpl.TxResponse = + await this.client.submitAndWait(mint_token_multisig_tx); + const mintTokenMeta: NFTokenMintMetadata = mintTokenTx.result + .meta! as NFTokenMintMetadata; + if (mintTokenMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not mint tokens to user: ${mintTokenMeta!.TransactionResult}` + ); + } + } else { + console.log('No need to mint tokens, because this was a withdraw flow SSF'); + } + + console.log('Success! Now Doing the mint for SSF'); + // multisig mint + const mint_multisig_tx = xrpl.multisign(mintNFTSignedTxBlobs); + const mintTx: xrpl.TxResponse = + await this.client.submitAndWait(mint_multisig_tx); + const mintMeta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata; + if (mintMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not mint temporary Ripple Vault: ${mintMeta!.TransactionResult}` + ); + } + } catch (error) { + throw new RippleError(`Unable to set Vault status to FUNDED: ${error}`); + } + }); + } + + async performCheckCashAndNftUpdate( + cashCheckSignedTxBlobs: string[], + burnNFTSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log('Doing the check cashing'); + // multisig burn + const cash_check_tx = xrpl.multisign(cashCheckSignedTxBlobs); + const cashCheckTx: xrpl.TxResponse = + await this.client.submitAndWait(cash_check_tx); // add timeouts + const cashCheckMeta: NFTokenMintMetadata = cashCheckTx.result.meta! as NFTokenMintMetadata; + if (cashCheckMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError(`Could not cash check: ${cashCheckMeta!.TransactionResult}`); + } + + console.log('Doing the burn for SSP'); + // multisig burn + const burn_multisig_tx = xrpl.multisign(burnNFTSignedTxBlobs); + const burnTx: xrpl.TxResponse = + await this.client.submitAndWait(burn_multisig_tx); // add timeouts + const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; + if (burnMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not burn temporary Ripple Vault: ${burnMeta!.TransactionResult}` + ); + } + + console.log('Success! Now Doing the mint for SSP'); + + // multisig mint + const mint_multisig_tx = xrpl.multisign(mintNFTSignedTxBlobs); + const mintTx: xrpl.TxResponse = + await this.client.submitAndWait(mint_multisig_tx); // add timeouts + const mintMeta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata; + if (mintMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not mint temporary Ripple Vault: ${mintMeta!.TransactionResult}` + ); + } + + console.log('Success! Done with the mint for SSP'); + } catch (error) { + throw new RippleError(`Unable to set Vault status to PENDING: ${error}`); + } + }); + } + + async setVaultStatusPending( + burnNFTSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log('Doing the burn for SSP'); + // multisig burn + const burn_multisig_tx = xrpl.multisign(burnNFTSignedTxBlobs); + const burnTx: xrpl.TxResponse = + await this.client.submitAndWait(burn_multisig_tx); + const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; + if (burnMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not burn temporary Ripple Vault: ${burnMeta!.TransactionResult}` + ); + } + + console.log('Success! Now Doing the mint for SSP'); + + // multisig mint + const mint_multisig_tx = xrpl.multisign(mintNFTSignedTxBlobs); + const mintTx: xrpl.TxResponse = + await this.client.submitAndWait(mint_multisig_tx); + const mintMeta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata; + if (mintMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not mint temporary Ripple Vault: ${mintMeta!.TransactionResult}` + ); + } + + console.log('Success! Done with the mint for SSP'); + } catch (error) { + throw new RippleError(`Unable to set Vault status to PENDING: ${error}`); + } + }); + } + + async getContractVaults(): Promise { + return await this.withConnectionMgmt(async () => { + try { + return await getAllRippleVaults(this.client, this.issuerAddress); + } catch (error) { + throw new RippleError(`Could not fetch All Vaults: ${error}`); + } + }); + } + + async getNFTokenIdForVault(uuid: string): Promise { + return await this.withConnectionMgmt(async () => { + console.log(`Getting NFTokenId for vault: ${uuid}`); + try { + const getNFTsTransaction: AccountNFTsRequest = { + command: 'account_nfts', + account: this.issuerAddress, + limit: 400, + }; + + const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); + const matchingNFT = nfts.result.account_nfts.find( + nft => decodeURI(nft.URI!).uuid.slice(2) === uuid + ); + + if (!matchingNFT) { + throw new RippleError(`Vault for uuid: ${uuid} not found`); + } + return matchingNFT.NFTokenID; + } catch (error) { + throw new RippleError(`Could not find NFTokenId for vault Vault: ${error}`); + } + }); + } + + async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log(`Getting sig for Burning Ripple Vault, vault: ${nftUUID}`); + const nftTokenId = await this.getNFTokenIdForVault(nftUUID); + const burnTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenBurn', + Account: this.issuerAddress, + NFTokenID: nftTokenId, + }; + const preparedBurnTx = await this.client.autofill(burnTransactionJson, this.minSigners); + + // set the LastLedgerSequence to be rounded up to the nearest 10000. + // this is to ensure that the transaction is valid for a while, and that the different attestors all use a matching LLS value to have matching sigs + // The request has a timeout, so this shouldn't end up being a hanging request + // Using the ticket system would likely be a better way: + // https://xrpl.org/docs/concepts/accounts/tickets + preparedBurnTx.LastLedgerSequence = + Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; + + if (incrementBy > 0) { + preparedBurnTx.Sequence = preparedBurnTx.Sequence! + incrementBy; + } + + console.log('preparedBurnTx ', preparedBurnTx); + + const sig = this.wallet.sign(preparedBurnTx, true); + // console.log('tx_one_sig: ', sig); + return sig.tx_blob; + } catch (error) { + throw new RippleError(`Could not burn Vault: ${error}`); + } + }); + } + + async mintNFT(vault: RawVault, incrementBy: number = 0): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log( + `Getting sig for Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}` + ); + const newURI = encodeURI(vault); + console.log('newURI: ', newURI); + const mintTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenMint', + Account: this.issuerAddress, + URI: newURI, + NFTokenTaxon: 0, + }; + const preparedMintTx = await this.client.autofill(mintTransactionJson, this.minSigners); + + // set the LastLedgerSequence to be rounded up to the nearest 10000. + // this is to ensure that the transaction is valid for a while, and that the different attestors all use a matching LLS value to have matching sigs + // The request has a timeout, so this shouldn't end up being a hanging request + // Using the ticket system would likely be a better way: + // https://xrpl.org/docs/concepts/accounts/tickets + preparedMintTx.LastLedgerSequence = + Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; + if (incrementBy > 0) { + preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; + } + + console.log('preparedMintTx ', preparedMintTx); + + const sig = this.wallet.sign(preparedMintTx, true); + console.log('tx_one_sig: ', sig); + return sig.tx_blob; + } catch (error) { + throw new RippleError(`Could not mint Vault: ${error}`); + } + }); + } + + async getSigUpdateVaultForSSP(uuid: string, updates: SSPVaultUpdate): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log(`Getting sig for getSigUpdateVaultForSSP, vault uuid: ${uuid}`); + const nftUUID = uuid; + const thisVault = await this.getRawVault(nftUUID); + console.log(`the vault, vault: `, thisVault); + const updatedVault = { + ...thisVault, + status: updates.status, + wdTxId: updates.wdTxId, + taprootPubKey: updates.taprootPubKey, + }; + console.log(`the updated vault, vault: `, updatedVault); + return await this.mintNFT(updatedVault, 1); + } catch (error) { + throw new RippleError(`Could not update Vault: ${error}`); + } + }); + } + + async getSigUpdateVaultForSSF( + uuid: string, + updates: SSFVaultUpdate, + updateSequenceBy: number + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + const nftUUID = uuid; + const thisVault = await this.getRawVault(nftUUID); + const updatedVault = { + ...thisVault, + status: updates.status, + fundingTxId: updates.fundingTxId, + wdTxId: updates.wdTxId, + valueMinted: BigNumber.from(updates.valueMinted), + valueLocked: BigNumber.from(updates.valueLocked), + }; + return await this.mintNFT(updatedVault, updateSequenceBy); + } catch (error) { + throw new RippleError(`Could not update Vault: ${error}`); + } + }); + } + + async getAllChecks(): Promise { + return await this.withConnectionMgmt(async () => { + try { + const getAccountObjectsRequestJSON: Request = { + command: 'account_objects', + account: this.issuerAddress, + ledger_index: 'validated', + type: 'check', + }; + + const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request( + getAccountObjectsRequestJSON + ); + + return getAccountObjectsResponse.result.account_objects; + } catch (error) { + throw new RippleError(`Could not fetch Checks: ${error}`); + } + }); + } + + async getCashCheckAndWithdrawSignatures(txHash: string): Promise { + return await this.withConnectionMgmt(async () => { + try { + const check = await getCheckByTXHash(this.client, this.issuerAddress, txHash); + const invoiceID = check.InvoiceID; + + if (!invoiceID) { + throw new RippleError(`Could not find Invoice ID for Check with TX Hash: ${txHash}`); + } + + const vault = await this.getRawVault(`0x${invoiceID}`.toLowerCase()); + + if (!vault) { + throw new RippleError( + `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` + ); + } + + const checkSendMax = check.SendMax as IssuedCurrencyAmount; + + const checkCashSignatures = await this.cashCheck(check.index, checkSendMax.value); + + const mintAndBurnSignatures = await this.withdraw( + vault.uuid, + BigInt(shiftValue(Number(checkSendMax.value))) + ); + return [checkCashSignatures, ...mintAndBurnSignatures]; + } catch (error) { + throw new RippleError(`Could not get Cash Check and Withdraw Signatures: ${error}`); + } + }); + } + + async cashCheck(checkID: string, dlcBTCAmount: string): Promise { + return await this.withConnectionMgmt(async () => { + try { + console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`); + + const cashCheckTransactionJSON: CheckCash = { + TransactionType: 'CheckCash', + Account: this.issuerAddress, + CheckID: checkID, + Amount: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + value: dlcBTCAmount, + issuer: this.issuerAddress, + }, + }; + + const updatedCashCheckTransactionJSON: CheckCash = await this.client.autofill( + cashCheckTransactionJSON, + this.minSigners + ); + + // set the LastLedgerSequence to be rounded up to the nearest 10000. + // this is to ensure that the transaction is valid for a while, and that the different attestors all use a matching LLS value to have matching sigs + // The request has a timeout, so this shouldn't end up being a hanging request + // Using the ticket system would likely be a better way: + // https://xrpl.org/docs/concepts/accounts/tickets + updatedCashCheckTransactionJSON.LastLedgerSequence = + Math.ceil(updatedCashCheckTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; + + console.log( + 'Issuer is about to sign the following cashCheck tx: ', + updatedCashCheckTransactionJSON + ); + + const signCashCheckTransactionSig: SignResponse = this.wallet.sign( + updatedCashCheckTransactionJSON, + true + ); + + return signCashCheckTransactionSig.tx_blob; + } catch (error) { + throw new RippleError(`Could not cash Check: ${error}`); + } + }); + } + + async mintTokens( + updatedValueMinted: number, + destinationAddress: string, + valueMinted: number, + incrementBy: number = 0 + ): Promise { + return await this.withConnectionMgmt(async () => { + try { + if (updatedValueMinted === 0 || valueMinted >= updatedValueMinted) { + console.log('No need to mint tokens, because this is a withdraw SSF'); + return ''; + } + const mintValue = unshiftValue( + new Decimal(updatedValueMinted).minus(valueMinted).toNumber() + ); + const dlcBTCAmount = mintValue.toString(); + console.log(`Minting ${dlcBTCAmount} dlcBTC to ${destinationAddress} address`); + + const sendTokenTransactionJSON: Payment = { + TransactionType: 'Payment', + Account: this.issuerAddress, + Destination: destinationAddress, + DestinationTag: 1, + Amount: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + value: dlcBTCAmount, + issuer: this.issuerAddress, + }, + }; + + const updatedSendTokenTransactionJSON: Payment = await this.client.autofill( + sendTokenTransactionJSON, + this.minSigners + ); + + // set the LastLedgerSequence to be rounded up to the nearest 10000. + // this is to ensure that the transaction is valid for a while, and that the different attestors all use a matching LLS value to have matching sigs + // The request has a timeout, so this shouldn't end up being a hanging request + // Using the ticket system would likely be a better way: + // https://xrpl.org/docs/concepts/accounts/tickets + updatedSendTokenTransactionJSON.LastLedgerSequence = + Math.ceil(updatedSendTokenTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; + + if (incrementBy > 0) { + updatedSendTokenTransactionJSON.Sequence = + updatedSendTokenTransactionJSON.Sequence! + incrementBy; + } + + console.log( + 'Issuer is about to sign the following mintTokens tx: ', + updatedSendTokenTransactionJSON + ); + + const signSendTokenTransactionResponse: SignResponse = this.wallet.sign( + updatedSendTokenTransactionJSON, + true + ); + + return signSendTokenTransactionResponse.tx_blob; + } catch (error) { + throw new RippleError(`Could not mint tokens: ${error}`); + } + }); + } +} diff --git a/src/network-handlers/xrp-gem-wallet-handler.ts b/src/network-handlers/xrp-gem-wallet-handler.ts new file mode 100644 index 0000000..3f5ff56 --- /dev/null +++ b/src/network-handlers/xrp-gem-wallet-handler.ts @@ -0,0 +1,159 @@ +import { getAddress, signTransaction } from '@gemwallet/api'; +import { ResponseType } from '@gemwallet/api/_constants/index.js'; +import { CheckCreate, Client, TrustSet } from 'xrpl'; + +import { submitXRPLCheckToCash } from '../functions/attestor/attestor-request.functions.js'; +import { + checkRippleTransactionResult, + connectRippleClient, + createCheck, + getDLCBTCBalance, + getLockedBTCBalance, + setTrustLine, +} from '../functions/ripple/ripple.functions.js'; +import { AttestorChainID } from '../models/attestor.models.js'; + +export class GemXRPHandler { + private xrpClient: Client; + private issuerAddress: string; + private userAddress: string; + + constructor(xrpClient: Client, issuerAddress: string, userAddress: string) { + this.xrpClient = xrpClient; + this.issuerAddress = issuerAddress; + this.userAddress = userAddress; + } + + public async getAddress(): Promise { + const getAddressResponse = await getAddress(); + + if (getAddressResponse.type === ResponseType.Reject || !getAddressResponse.result) { + throw new Error('Error getting Address'); + } + return getAddressResponse.result.address; + } + + public async setTrustLine(): Promise { + try { + const trustLineRequest = await setTrustLine( + this.xrpClient, + this.userAddress, + this.issuerAddress + ); + + if (!trustLineRequest) { + console.error('TrustLine is already set'); + return; + } + const updatedTrustLineRequest: TrustSet = { + ...trustLineRequest, + Flags: 2147483648, + }; + + const signTrustLineResponse = await signTransaction({ transaction: updatedTrustLineRequest }); + + if ( + signTrustLineResponse.type === ResponseType.Reject || + !signTrustLineResponse.result || + !signTrustLineResponse.result.signature + ) { + throw new Error('Error signing Trust Line'); + } + + const signedTrustLineRequest = signTrustLineResponse.result.signature; + + await connectRippleClient(this.xrpClient); + + const submitTrustLineRequestResponse = + await this.xrpClient.submitAndWait(signedTrustLineRequest); + + console.log(`Response for submitted Transaction Request:`, submitTrustLineRequestResponse); + + checkRippleTransactionResult(submitTrustLineRequestResponse); + } catch (error) { + throw new Error(`Error setting Trust Line: ${error}`); + } + } + + public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { + try { + const checkCreateRequest: CheckCreate = await createCheck( + this.xrpClient, + this.userAddress, + this.issuerAddress, + undefined, + dlcBTCAmount, + vaultUUID + ); + + const updatedCheckCreateRequest: CheckCreate = { + ...checkCreateRequest, + Flags: 2147483648, + }; + + return updatedCheckCreateRequest; + } catch (error) { + throw new Error(`Error creating Check: ${error}`); + } + } + + public async signAndSubmitCheck(checkCreateRequest: CheckCreate): Promise { + try { + const signCheckCreateResponse = await signTransaction({ + transaction: checkCreateRequest, + }); + + if ( + signCheckCreateResponse.type === ResponseType.Reject || + !signCheckCreateResponse.result || + !signCheckCreateResponse.result.signature + ) { + throw new Error('Error signing Check Create'); + } + + const signedCheckCreateRequest = signCheckCreateResponse.result.signature; + + await connectRippleClient(this.xrpClient); + + const submitCheckCreateRequestResponse = + await this.xrpClient.submitAndWait(signedCheckCreateRequest); + + console.log(`Response for submitted Transaction Request:`, submitCheckCreateRequestResponse); + + checkRippleTransactionResult(submitCheckCreateRequestResponse); + + return submitCheckCreateRequestResponse.result.hash; + } catch (error) { + throw new Error(`Error signing and submitting Check: ${error}`); + } + } + + public async sendCheckTXHash( + coordinatorURL: string, + checkTXHash: string, + attestorChainID: AttestorChainID + ): Promise { + try { + await submitXRPLCheckToCash(coordinatorURL, checkTXHash, attestorChainID); + } catch (error) { + throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); + } + } + public async getDLCBTCBalance(): Promise { + try { + await connectRippleClient(this.xrpClient); + return await getDLCBTCBalance(this.xrpClient, this.userAddress, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } + + public async getLockedBTCBalance(): Promise { + try { + await connectRippleClient(this.xrpClient); + return await getLockedBTCBalance(this.xrpClient, this.userAddress, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } +} diff --git a/src/network-handlers/xrp-ledger-handler.ts b/src/network-handlers/xrp-ledger-handler.ts new file mode 100644 index 0000000..17deb1b --- /dev/null +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -0,0 +1,170 @@ +import * as Xrp from '@ledgerhq/hw-app-xrp'; +import { encode } from 'ripple-binary-codec'; +import { CheckCreate, Client, Transaction, TrustSet } from 'xrpl'; + +import { submitXRPLCheckToCash } from '../functions/attestor/attestor-request.functions.js'; +import { + checkRippleTransactionResult, + connectRippleClient, + createCheck, + getDLCBTCBalance, + getLockedBTCBalance, + setTrustLine, +} from '../functions/ripple/ripple.functions.js'; +import { AttestorChainID } from '../models/attestor.models.js'; + +export class LedgerXRPHandler { + private ledgerApp: Xrp.default; + private derivationPath: string; + private xrpClient: Client; + private issuerAddress: string; + private userAddress: string; + private publicKey: string; + + constructor( + ledgerApp: Xrp.default, + derivationPath: string, + xrpClient: Client, + issuerAddress: string, + userAddress: string, + publicKey: string + ) { + this.ledgerApp = ledgerApp; + this.derivationPath = derivationPath; + this.xrpClient = xrpClient; + this.issuerAddress = issuerAddress; + this.userAddress = userAddress; + this.publicKey = publicKey; + } + + public async getAddress(): Promise { + const address = await this.ledgerApp.getAddress(this.derivationPath); + return address.address; + } + + public async setTrustLine(): Promise { + try { + const trustLineRequest = await setTrustLine( + this.xrpClient, + this.userAddress, + this.issuerAddress + ); + + if (!trustLineRequest) { + console.error('TrustLine is already set'); + return; + } + const updatedTrustLineRequest: TrustSet = { + ...trustLineRequest, + Flags: 2147483648, + SigningPubKey: this.publicKey.toUpperCase(), + }; + + const encodedTrustLineRequest = encode(updatedTrustLineRequest); + + const signature = await this.ledgerApp.signTransaction( + this.derivationPath, + encodedTrustLineRequest + ); + console.log('Signature:', signature); + + const signedTrustLineRequest: TrustSet = { + ...updatedTrustLineRequest, + TxnSignature: signature, + }; + + await connectRippleClient(this.xrpClient); + + const submitTrustLineRequestResponse = + await this.xrpClient.submitAndWait(signedTrustLineRequest); + + console.log(`Response for submitted Transaction Request:`, submitTrustLineRequestResponse); + + checkRippleTransactionResult(submitTrustLineRequestResponse); + } catch (error) { + throw new Error(`Error setting Trust Line: ${error}`); + } + } + + public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { + try { + const checkCreateRequest: CheckCreate = await createCheck( + this.xrpClient, + this.userAddress, + this.issuerAddress, + undefined, + dlcBTCAmount, + vaultUUID + ); + + const updatedCheckCreateRequest: CheckCreate = { + ...checkCreateRequest, + Flags: 2147483648, + SigningPubKey: this.publicKey.toUpperCase(), + }; + + return updatedCheckCreateRequest; + } catch (error) { + throw new Error(`Error creating Check: ${error}`); + } + } + + public async signAndSubmitCheck(checkCreateRequest: CheckCreate): Promise { + try { + const encodedCheckCreateRequest = encode(checkCreateRequest); + + const signature = await this.ledgerApp.signTransaction( + this.derivationPath, + encodedCheckCreateRequest + ); + + const signedCheckCreateRequest: Transaction = { + ...checkCreateRequest, + TxnSignature: signature, + }; + + await connectRippleClient(this.xrpClient); + + const submitCheckCreateRequestResponse = + await this.xrpClient.submitAndWait(signedCheckCreateRequest); + + console.log(`Response for submitted Transaction Request:`, submitCheckCreateRequestResponse); + + checkRippleTransactionResult(submitCheckCreateRequestResponse); + + return submitCheckCreateRequestResponse.result.hash; + } catch (error) { + throw new Error(`Error signing and submitting Check: ${error}`); + } + } + + public async sendCheckTXHash( + coordinatorURL: string, + checkTXHash: string, + attestorChainID: AttestorChainID + ): Promise { + try { + await submitXRPLCheckToCash(coordinatorURL, checkTXHash, attestorChainID); + } catch (error) { + throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); + } + } + + public async getDLCBTCBalance(): Promise { + try { + await connectRippleClient(this.xrpClient); + return await getDLCBTCBalance(this.xrpClient, this.userAddress, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } + + public async getLockedBTCBalance(): Promise { + try { + await connectRippleClient(this.xrpClient); + return await getLockedBTCBalance(this.xrpClient, this.userAddress, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } +} diff --git a/tests/unit/request-functions.test.ts b/tests/unit/request-functions.test.ts index b7c0098..973f12d 100644 --- a/tests/unit/request-functions.test.ts +++ b/tests/unit/request-functions.test.ts @@ -26,7 +26,7 @@ describe('Request Functions', () => { ); await expect(sendRequest(TEST_REGTEST_ATTESTOR_APIS[0], 'requestBody')).rejects.toThrow( - new Error(`Response ${TEST_REGTEST_ATTESTOR_APIS[0]} was not OK: Bad Request`) + new Error(`Request to ${TEST_REGTEST_ATTESTOR_APIS[0]} failed: Bad Request - `) ); }); diff --git a/yarn.lock b/yarn.lock index 8aa3761..d129c05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -764,6 +764,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@gemwallet/api@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@gemwallet/api/-/api-3.8.0.tgz#46bc47789848c7ac9cc620613e0a1757dc8668a1" + integrity sha512-hZ6XC0mVm3Q54cgonrzk6tHS/wUMjtPHyqsqbtlnNGPouCR7OIfEDo5Y802qLZ5ah6PskhsK0DouVnwUykEM8Q== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1067,17 +1072,32 @@ rxjs "^7.8.1" semver "^7.3.5" +"@ledgerhq/devices@^8.4.4": + version "8.4.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.4.tgz#0d195c1650fe57da2fad7f0d9074a0190947cd6f" + integrity sha512-sz/ryhe/R687RHtevIE9RlKaV8kkKykUV4k29e7GAVwzHX1gqG+O75cu1NCJUHLbp3eABV5FdvZejqRUlLis9A== + dependencies: + "@ledgerhq/errors" "^6.19.1" + "@ledgerhq/logs" "^6.12.0" + rxjs "^7.8.1" + semver "^7.3.5" + "@ledgerhq/errors@^6.16.4": version "6.16.4" resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.16.4.tgz#a38baffe8b096d9fff3ad839cadb55704c8d8e7b" integrity sha512-M57yFaLYSN+fZCX0E0zUqOmrV6eipK+s5RhijHoUNlHUqrsvUz7iRQgpd5gRgHB5VkIjav7KdaZjKiWGcHovaQ== -"@ledgerhq/hw-app-btc@^10.2.4": - version "10.2.4" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-10.2.4.tgz#0cecc13ba6d1d309d7bd938afe290f6cdd48f061" - integrity sha512-Uy4v4St6GKI2nyKpplTpIFyp8bHRFEIuhm5YtaUr8JbgkuqlnV7p/d2/uzP7jY6cMArGRrdmJEtuIuvLUdRYrw== +"@ledgerhq/errors@^6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.1.tgz#d9ac45ad4ff839e468b8f63766e665537aaede58" + integrity sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw== + +"@ledgerhq/hw-app-btc@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-btc/-/hw-app-btc-10.4.1.tgz#c78d97ec2515ae897bf3256a046934ddcd924aa9" + integrity sha512-8EpI59hT9N+kAN5kUE9F2DVzBTH7RSdXoyK5+l4UulJTzhJSh7888YvE//iDY+IiUOsNwMiHWxgAmZN3LucKQQ== dependencies: - "@ledgerhq/hw-transport" "^6.30.6" + "@ledgerhq/hw-transport" "^6.31.2" "@ledgerhq/logs" "^6.12.0" bip32-path "^0.4.2" bitcoinjs-lib "^5.2.0" @@ -1090,7 +1110,15 @@ tiny-secp256k1 "1.1.6" varuint-bitcoin "1.1.2" -"@ledgerhq/hw-transport@^6.20.0", "@ledgerhq/hw-transport@^6.30.6": +"@ledgerhq/hw-app-xrp@6.29.4": + version "6.29.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-xrp/-/hw-app-xrp-6.29.4.tgz#b90d847f2cf8ddca9ca0068f5079f199c334314d" + integrity sha512-fEnqkwmEmcThGVtxLUQX9x4KB1E659Ke1dYuCZSXX4346+h3PCa7cfeKN/VRZNH8HQJgiYi53LqqYzwWXB5zbg== + dependencies: + "@ledgerhq/hw-transport" "^6.31.4" + bip32-path "0.4.2" + +"@ledgerhq/hw-transport@^6.20.0": version "6.30.6" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.30.6.tgz#c6d84672ac4828f311831998f4101ea205215a6d" integrity sha512-fT0Z4IywiuJuZrZE/+W0blkV5UCotDPFTYKLkKCLzYzuE6javva7D/ajRaIeR+hZ4kTmKF4EqnsmDCXwElez+w== @@ -1100,11 +1128,28 @@ "@ledgerhq/logs" "^6.12.0" events "^3.3.0" +"@ledgerhq/hw-transport@^6.31.2", "@ledgerhq/hw-transport@^6.31.4": + version "6.31.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.31.4.tgz#9b23a6de4a4caaa5c24b149c2dea8adde46f0eb1" + integrity sha512-6c1ir/cXWJm5dCWdq55NPgCJ3UuKuuxRvf//Xs36Bq9BwkV2YaRQhZITAkads83l07NAdR16hkTWqqpwFMaI6A== + dependencies: + "@ledgerhq/devices" "^8.4.4" + "@ledgerhq/errors" "^6.19.1" + "@ledgerhq/logs" "^6.12.0" + events "^3.3.0" + "@ledgerhq/logs@^6.12.0": version "6.12.0" resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.12.0.tgz#ad903528bf3687a44da435d7b2479d724d374f5d" integrity sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA== +"@noble/curves@^1.0.0", "@noble/curves@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/curves@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" @@ -1112,11 +1157,16 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@1.4.0", "@noble/hashes@^1.1.5", "@noble/hashes@^1.2.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0": +"@noble/hashes@1.4.0", "@noble/hashes@^1.1.5", "@noble/hashes@^1.2.0", "@noble/hashes@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.5.0", "@noble/hashes@^1.0.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + "@noble/secp256k1@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -1148,12 +1198,39 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== -"@scure/base@^1.1.1", "@scure/base@^1.1.6", "@scure/base@~1.1.5", "@scure/base@~1.1.6": +"@scure/base@1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.8.tgz#8f23646c352f020c83bca750a82789e246d42b50" + integrity sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg== + +"@scure/base@^1.1.1", "@scure/base@~1.1.5", "@scure/base@~1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== -"@scure/btc-signer@^1.3.1": +"@scure/base@^1.1.3", "@scure/base@~1.1.7", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + +"@scure/bip32@^1.3.1": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + +"@scure/bip39@^1.2.1": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + +"@scure/btc-signer@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-1.3.2.tgz#56cf02a2e318097b1e4f531fac8ef114bdf4ddc8" integrity sha512-BmcQHvxaaShKwgbFC0vDk0xzqbMhNtNmgXm6u7cz07FNtGsVItUuHow6NbgHmc+oJSBZJRym5dz8+Uu0JoEJhQ== @@ -1301,7 +1378,7 @@ "@types/node" "*" kleur "^3.0.3" -"@types/ramda@^0.30.1": +"@types/ramda@0.30.1": version "0.30.1" resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.30.1.tgz#316257fec12747bb39a2e921df48a9dcb8c164a9" integrity sha512-aoyF/ADPL6N+/NXXfhPWF+Qj6w1Cql59m9wX0Gi15uyF+bpzXeLd63HPdiTDE2bmLXfNcVufsDPKmbfOrOzTBA== @@ -1457,6 +1534,23 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@xrplf/isomorphic@^1.0.0", "@xrplf/isomorphic@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@xrplf/isomorphic/-/isomorphic-1.0.1.tgz#d7676e0ec0e55a39f37ddc1f3cc30eeab52e0739" + integrity sha512-0bIpgx8PDjYdrLFeC3csF305QQ1L7sxaWnL5y71mCvhenZzJgku9QsA+9QCXBC1eNYtxWO/xR91zrXJy2T/ixg== + dependencies: + "@noble/hashes" "^1.0.0" + eventemitter3 "5.0.1" + ws "^8.13.0" + +"@xrplf/secret-numbers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@xrplf/secret-numbers/-/secret-numbers-1.0.0.tgz#cc19ff84236cc2737b38f2e42a29924f2b8ffc0e" + integrity sha512-qsCLGyqe1zaq9j7PZJopK+iGTGRbk6akkg6iZXJJgxKwck0C5x5Gnwlb1HKYGOwPKyrXWpV6a2YmcpNpUFctGg== + dependencies: + "@xrplf/isomorphic" "^1.0.0" + ripple-keypairs "^2.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1694,6 +1788,11 @@ bech32@^2.0.0: resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + bindings@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1706,11 +1805,21 @@ bip174@^2.0.1, bip174@^2.1.1: resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.1.tgz#ef3e968cf76de234a546962bcf572cc150982f9f" integrity sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ== -bip32-path@^0.4.2: +bip32-path@0.4.2, bip32-path@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/bip32-path/-/bip32-path-0.4.2.tgz#5db0416ad6822712f077836e2557b8697c0c7c99" integrity sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ== +bip32@4.0.0, bip32@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-4.0.0.tgz#7fac3c05072188d2d355a4d6596b37188f06aa2f" + integrity sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ== + dependencies: + "@noble/hashes" "^1.2.0" + "@scure/base" "^1.1.1" + typeforce "^1.11.5" + wif "^2.0.6" + bip32@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.6.tgz#6a81d9f98c4cd57d05150c60d8f9e75121635134" @@ -1724,16 +1833,6 @@ bip32@^2.0.4: typeforce "^1.11.5" wif "^2.0.6" -bip32@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/bip32/-/bip32-4.0.0.tgz#7fac3c05072188d2d355a4d6596b37188f06aa2f" - integrity sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ== - dependencies: - "@noble/hashes" "^1.2.0" - "@scure/base" "^1.1.1" - typeforce "^1.11.5" - wif "^2.0.6" - bip66@^1.1.0: version "1.1.5" resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" @@ -1751,6 +1850,18 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0: resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== +bitcoinjs-lib@6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz#f57c17c82511f860f11946d784c18da39f8618a8" + integrity sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA== + dependencies: + "@noble/hashes" "^1.2.0" + bech32 "^2.0.0" + bip174 "^2.1.1" + bs58check "^3.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + bitcoinjs-lib@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-5.2.0.tgz#caf8b5efb04274ded1b67e0706960b93afb9d332" @@ -1772,7 +1883,7 @@ bitcoinjs-lib@^5.2.0: varuint-bitcoin "^1.0.4" wif "^2.0.1" -bitcoinjs-lib@^6.1.3, bitcoinjs-lib@^6.1.5: +bitcoinjs-lib@^6.1.3: version "6.1.5" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz#3b03509ae7ddd80a440f10fc38c4a97f0a028d8c" integrity sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ== @@ -1906,6 +2017,11 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +chalk@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1934,11 +2050,6 @@ chalk@^4.0.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -2185,7 +2296,7 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.4.3: +decimal.js@10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -2547,6 +2658,11 @@ ethers@5.7.2: "@ethersproject/web" "5.7.1" "@ethersproject/wordlists" "5.7.0" +eventemitter3@5.0.1, eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3612,7 +3728,7 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -ledger-bitcoin@^0.2.3: +ledger-bitcoin@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/ledger-bitcoin/-/ledger-bitcoin-0.2.3.tgz#9954f4464f1f439e393d228ae477ad86573d023b" integrity sha512-sWdvMTR5CkebNlM0Mam9ROdpsD7Y4087kj4cbIaCCq8IXShCQ44vE3j0wTmt+sHp13eETgY63OWN1rkuIfMfuQ== @@ -4174,7 +4290,7 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prompts@^2.0.1, prompts@^2.4.2: +prompts@2.4.2, prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -4214,7 +4330,7 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -ramda@^0.30.1: +ramda@0.30.1: version "0.30.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.1.tgz#7108ac95673062b060025052cd5143ae8fc605bf" integrity sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw== @@ -4354,6 +4470,32 @@ ripemd160@2, ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ripple-address-codec@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-5.0.0.tgz#97059f7bba6f9ed7a52843de8aa427723fb529f6" + integrity sha512-de7osLRH/pt5HX2xw2TRJtbdLLWHu0RXirpQaEeCnWKY5DYHykh3ETSkofvm0aX0LJiV7kwkegJxQkmbO94gWw== + dependencies: + "@scure/base" "^1.1.3" + "@xrplf/isomorphic" "^1.0.0" + +ripple-binary-codec@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-2.1.0.tgz#f1ef81f8d1f05a6cecc06fc6d9b13456569cafda" + integrity sha512-q0GAx+hj3UVcDbhXVjk7qeNfgUMehlElYJwiCuIBwqs/51GVTOwLr39Ht3eNsX5ow2xPRaC5mqHwcFDvLRm6cA== + dependencies: + "@xrplf/isomorphic" "^1.0.1" + bignumber.js "^9.0.0" + ripple-address-codec "^5.0.0" + +ripple-keypairs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-2.0.0.tgz#4a1a8142e9a58c07e61b3cc6cfe7317db718d289" + integrity sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag== + dependencies: + "@noble/curves" "^1.0.0" + "@xrplf/isomorphic" "^1.0.0" + ripple-address-codec "^5.0.0" + run-async@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -4395,7 +4537,7 @@ scrypt-js@3.0.1: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -scure@^1.6.0: +scure@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/scure/-/scure-1.6.0.tgz#1d16ac36c0ae601860f28cc01c202fef796eb1da" integrity sha512-IiQqkgU1J7gqDPIjUd1EpPQ4HAyqRXFrva+/IMh0u6dHNVdzpoOS4NvehZi4HAM3Op8DzsAmbQSCskCDgffJcw== @@ -4677,7 +4819,7 @@ tiny-secp256k1@1.1.6, tiny-secp256k1@^1.1.1, tiny-secp256k1@^1.1.3: elliptic "^6.4.0" nan "^2.13.2" -tiny-secp256k1@^2.2.3: +tiny-secp256k1@2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz#fe1dde11a64fcee2091157d4b78bcb300feb9b65" integrity sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q== @@ -5024,6 +5166,26 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xrpl@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xrpl/-/xrpl-4.0.0.tgz#c031b848c2a3e955b69b1dd438b1156e6826a2d6" + integrity sha512-VZm1lQWHQ6PheAAFGdH+ISXKvqB2hZDQ0w4ZcdAEtmqZQXtSIVQHOKPz95rEgGANbos7+XClxJ73++joPhA8Cw== + dependencies: + "@scure/bip32" "^1.3.1" + "@scure/bip39" "^1.2.1" + "@xrplf/isomorphic" "^1.0.1" + "@xrplf/secret-numbers" "^1.0.0" + bignumber.js "^9.0.0" + eventemitter3 "^5.0.1" + ripple-address-codec "^5.0.0" + ripple-binary-codec "^2.1.0" + ripple-keypairs "^2.0.0" + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"