From 0a35add702d104d86c2767c1d5846c6ef37f19a0 Mon Sep 17 00:00:00 2001 From: sosaucily Date: Wed, 25 Sep 2024 12:26:05 +0200 Subject: [PATCH 01/27] Add initial support for Ripple, RippleHandler --- package.json | 3 +- .../bitcoin/bitcoin-request-functions.ts | 4 +- src/index.ts | 2 + src/models/attestor.models.ts | 4 +- src/models/errors.ts | 7 + src/network-handlers/ripple-handler.ts | 469 ++++++++++++++++++ yarn.lock | 107 ++++ 7 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 src/network-handlers/ripple-handler.ts diff --git a/package.json b/package.json index 58c6ea0..e099081 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "prompts": "^2.4.2", "ramda": "^0.30.1", "scure": "^1.6.0", - "tiny-secp256k1": "^2.2.3" + "tiny-secp256k1": "^2.2.3", + "xrpl": "^4.0.0" } } 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/index.ts b/src/index.ts index e750e7c..1a25c2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ 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 { ProofOfReserveHandler } from './proof-of-reserve-handlers/proof-of-reserve-handler.js'; export { @@ -10,4 +11,5 @@ export { SoftwareWalletDLCHandler, EthereumHandler, ProofOfReserveHandler, + RippleHandler, }; diff --git a/src/models/attestor.models.ts b/src/models/attestor.models.ts index 92b9bd2..bd1fe88 100644 --- a/src/models/attestor.models.ts +++ b/src/models/attestor.models.ts @@ -3,7 +3,9 @@ export type AttestorChainID = | 'evm-arbsepolia' | 'evm-localhost' | 'evm-hardhat-arb' - | 'evm-hardhat-eth'; + | 'evm-hardhat-eth' + | 'ripple-xrpl-mainnet' + | 'ripple-xrpl-testnet'; 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/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts new file mode 100644 index 0000000..7f35684 --- /dev/null +++ b/src/network-handlers/ripple-handler.ts @@ -0,0 +1,469 @@ +import { BigNumber } from 'ethers'; +import xrpl, { AccountNFTsRequest, SubmittableTransaction } from 'xrpl'; +import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; + +import { RippleError } from '../models/errors.js'; +import { RawVault, VaultState } from '../models/ethereum-models.js'; + +function encodeNftURI(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'); + console.log( + `UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}` + ); + let uuid = vault.uuid; + if (uuid === '') { + uuid = vault.uuid.padStart(64, '0'); + } + if (uuid.substring(0, 2) === '0x') { + uuid = uuid.slice(2); + } + // add padding so valueLocked and valueMinted are always 16 characters + const valueLockedPadded = vault.valueLocked._hex.substring(2).padStart(16, '0'); + const valueMintedPadded = vault.valueMinted._hex.substring(2).padStart(16, '0'); + const fundingTxId = vault.fundingTxId.padStart(64, '0'); + const wdTxId = vault.wdTxId.padStart(64, '0'); + const btcMintFeeBasisPoints = vault.btcMintFeeBasisPoints._hex.substring(2).padStart(2, '0'); + const btcRedeemFeeBasisPoints = vault.btcRedeemFeeBasisPoints._hex.substring(2).padStart(2, '0'); + const btcFeeRecipient = vault.btcFeeRecipient.padStart(64, '0'); + const taprootPubKey = vault.taprootPubKey.padStart(64, '0'); + console.log( + 'built URI:', + `${VERSION}${status}${uuid}${valueLockedPadded}${valueMintedPadded}${fundingTxId}${wdTxId}${btcMintFeeBasisPoints}${btcRedeemFeeBasisPoints}${btcFeeRecipient}${taprootPubKey}` + ); + return `${VERSION}${status}${uuid}${valueLockedPadded}${valueMintedPadded}${fundingTxId}${wdTxId}${btcMintFeeBasisPoints}${btcRedeemFeeBasisPoints}${btcFeeRecipient}${taprootPubKey}`; +} + +function decodeNftURI(URI: string): RawVault { + let VERSION = 0; + let uuid = ''; + let valueLocked = BigNumber.from(0); + let valueMinted = BigNumber.from(0); + let status = 0; + let fundingTxId = ''; + let wdTxId = ''; + let btcMintFeeBasisPoints = BigNumber.from(0); + let btcRedeemFeeBasisPoints = BigNumber.from(0); + let btcFeeRecipient = ''; + let taprootPubKey = ''; + try { + VERSION = parseInt(URI.slice(0, 2), 16); + status = parseInt(URI.slice(2, 4), 16); + console.log(`Decoding URI: ${URI}`); + uuid = URI.slice(4, 68); + valueLocked = BigNumber.from(`0x${URI.slice(68, 84)}`); + valueMinted = BigNumber.from(`0x${URI.slice(84, 100)}`); + fundingTxId = URI.slice(100, 164); + if (fundingTxId === '0'.repeat(64)) { + fundingTxId = ''; + } + wdTxId = URI.slice(164, 228); + if (wdTxId === '0'.repeat(64)) { + wdTxId = ''; + } + btcMintFeeBasisPoints = BigNumber.from(`0x${URI.slice(228, 230)}`); + btcRedeemFeeBasisPoints = BigNumber.from(`0x${URI.slice(230, 232)}`); + btcFeeRecipient = URI.slice(232, 298); + taprootPubKey = URI.slice(298, 362); + } catch (error) { + console.log(`Error decoding URI: ${error}`); + console.log(`URI which failed to Decode: ${URI}`); + return {} as RawVault; + } + console.log( + `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` + ); + const baseVault = buildDefaultNftVault(); + return { + ...baseVault, + uuid: `0x${uuid}`, + status: status, + valueLocked: valueLocked, + valueMinted: valueMinted, + fundingTxId: fundingTxId, + wdTxId: wdTxId, + btcMintFeeBasisPoints: btcMintFeeBasisPoints, + btcRedeemFeeBasisPoints: btcRedeemFeeBasisPoints, + btcFeeRecipient: btcFeeRecipient, + taprootPubKey: taprootPubKey, + }; +} + +function lowercaseHexFields(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(), + }; +} + +function buildDefaultNftVault(): RawVault { + return { + uuid: `0x${'0'.repeat(64)}`, + valueLocked: BigNumber.from(0), + valueMinted: BigNumber.from(0), + protocolContract: '', + timestamp: BigNumber.from(0), + creator: '', + 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 demo_wallet: xrpl.Wallet; + + private constructor() { + this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); + this.demo_wallet = xrpl.Wallet.fromSeed('sEdV6wSeVoUGwu7KHyFZ3UkrQxpvGZU'); //rNT2CxBbKtiUwy4UFwXS11PETZVW8j4k3g + } + + static fromWhatever(): RippleHandler { + return new RippleHandler(); + } + + async getNetworkInfo(): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + return await this.client.request({ command: 'server_info' }); + } catch (error) { + throw new RippleError(`Could not fetch Network Info: ${error}`); + } + } + + async getAddress(): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + return this.demo_wallet.classicAddress; + } catch (error) { + throw new RippleError(`Could not fetch Address Info: ${error}`); + } + } + + // static fromPrivateKey( + // ethereumDeploymentPlans: EthereumDeploymentPlan[], + // ethereumPrivateKey: string, + // rpcEndpoint: string + // ): EthereumHandler { + // const provider = getProvider(rpcEndpoint); + // const signer = new Wallet(ethereumPrivateKey, provider); + // const ethereumContracts = getEthereumContracts(ethereumDeploymentPlans, signer); + // return new EthereumHandler(ethereumContracts); + // } + + // static fromSigner( + // ethereumDeploymentPlans: EthereumDeploymentPlan[], + // signer: providers.JsonRpcSigner + // ): EthereumHandler { + // const ethereumContracts = getEthereumContracts(ethereumDeploymentPlans, signer); + // return new EthereumHandler(ethereumContracts); + // } + + // getContracts(): DLCEthereumContracts { + // return this.ethereumContracts; + // } + + // async getAllUserVaults(): Promise { + // try { + // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); + // return await getAllAddressVaults(this.ethereumContracts.dlcManagerContract, userAddress); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch all User Vaults: ${error}`); + // } + // } + + async getRawVault(uuid: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + const getNFTsTransaction: AccountNFTsRequest = { + command: 'account_nfts', + account: this.demo_wallet.classicAddress, + }; + let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; + nftUUID = nftUUID.toUpperCase(); + const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); + const nftTokenId = await this.getNFTokenIdForVault(nftUUID); + const matchingNFT = nfts.result.account_nfts.find(nft => nft.NFTokenID === nftTokenId); + if (!matchingNFT) { + throw new RippleError(`Vault with UUID: ${nftUUID} not found`); + } + const matchingVault: RawVault = decodeNftURI(matchingNFT.URI!); + return lowercaseHexFields(matchingVault); + } catch (error) { + throw new RippleError(`Could not fetch Vault: ${error}`); + } + } + + async setupVault(): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + const newVault = buildDefaultNftVault(); + const newVaultUUID = await this.mintNFT(newVault); + + await this.burnNFT(newVaultUUID); + + newVault.uuid = newVaultUUID; + await this.mintNFT(newVault); + + return `0x${newVaultUUID}`; + } catch (error) { + throw new RippleError(`Could not setup Ripple Vault: ${error}`); + } + } + + async withdraw(uuid: string, withdrawAmount: bigint) { + // Things like withdraw and deposit should get the existing NFT vault + // then burn the NFT, and mint a new one with the updated value + // putting the UUID into the URI + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; + nftUUID = nftUUID.toUpperCase(); + // return await withdraw(this.ethereumContracts.dlcManagerContract, vaultUUID, withdrawAmount); + const thisVault = await this.getRawVault(nftUUID); + await this.burnNFT(nftUUID); + thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount)); + await this.mintNFT(thisVault); + } catch (error) { + throw new RippleError(`Unable to perform Withdraw for User: ${error}`); + } + } + + async setVaultStatusFunded( + uuid: string, + bitcoinTransactionID: string, + updatedValueMinted: bigint + ): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; + nftUUID = nftUUID.toUpperCase(); + + console.log(`Setting Vault status to FUNDED, vault: ${nftUUID}`); + const thisVault = await this.getRawVault(nftUUID); + await this.burnNFT(nftUUID); + const newVault = { + ...thisVault, + status: VaultState.FUNDED, + fundingTxId: bitcoinTransactionID, + wdTxId: '', + valueMinted: BigNumber.from(updatedValueMinted), + valueLocked: BigNumber.from(updatedValueMinted), + }; + await this.mintNFT(newVault); + console.log(`Vault status set to FUNDED, vault: ${nftUUID}`); + } catch (error) { + throw new RippleError(`Unable to set Vault status to FUNDED: ${error}`); + } + } + + async setVaultStatusPending( + uuid: string, + bitcoinTransactionID: string, + updatedValueMinted: bigint, + userPubkey: string + ): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; + nftUUID = nftUUID.toUpperCase(); + + console.log(`Setting Vault status to Pending, vault: ${nftUUID}`); + const thisVault = await this.getRawVault(nftUUID); + await this.burnNFT(nftUUID); + const newVault = { + ...thisVault, + status: VaultState.PENDING, + wdTxId: bitcoinTransactionID, + taprootPubKey: userPubkey, + }; + await this.mintNFT(newVault); + console.log(`Vault status set to Pending, vault: ${nftUUID}`); + } catch (error) { + throw new RippleError(`Unable to set Vault status to FUNDED: ${error}`); + } + } + + // async getUserDLCBTCBalance(): Promise { + // try { + // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); + // return await getAddressDLCBTCBalance(this.ethereumContracts.dlcBTCContract, userAddress); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch User's dlcBTC balance: ${error}`); + // } + // } + + // async getDLCBTCTotalSupply(): Promise { + // try { + // return await getDLCBTCTotalSupply(this.ethereumContracts.dlcBTCContract); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch Total Supply of dlcBTC: ${error}`); + // } + // } + + // async getLockedBTCBalance(userVaults?: RawVault[]): Promise { + // try { + // if (!userVaults) { + // userVaults = await this.getAllUserVaults(); + // } + // return await getLockedBTCBalance(userVaults); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch Total Supply of Locked dlcBTC: ${error}`); + // } + // } + + // async getAttestorGroupPublicKey(): Promise { + // try { + // return getAttestorGroupPublicKey(this.ethereumContracts.dlcManagerContract); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch Attestor Public Key: ${error}`); + // } + // } + + // async isWhiteLisingEnabled(): Promise { + // try { + // return await isWhitelistingEnabled(this.ethereumContracts.dlcManagerContract); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch Whitelisting Status: ${error}`); + // } + // } + + // async isUserWhitelisted(): Promise { + // try { + // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); + // return await isUserWhitelisted(this.ethereumContracts.dlcManagerContract, userAddress); + // } catch (error) { + // throw new EthereumHandlerError(`Could not fetch User Whitelisting Status: ${error}`); + // } + // } + + async getContractVaults(): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + try { + const getNFTsTransaction: AccountNFTsRequest = { + command: 'account_nfts', + account: this.demo_wallet.classicAddress, + }; + + const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); + const allNFTs = nfts.result.account_nfts; + const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeNftURI(nft.URI!))); + + // allVaults.forEach((vault, index) => { + // if (vault.uuid === '') { + // vault.uuid = allNFTs[index].NFTokenID; + // } + // }); + + return allVaults; + } catch (error) { + throw new RippleError(`Could not fetch All Vaults: ${error}`); + } + } + + async getNFTokenIdForVault(uuid: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + console.log(`Getting NFTokenId for vault: ${uuid}`); + try { + const getNFTsTransaction: AccountNFTsRequest = { + command: 'account_nfts', + account: this.demo_wallet.classicAddress, + }; + + const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); + let matchingNFT = nfts.result.account_nfts.find( + nft => decodeNftURI(nft.URI!).uuid.slice(2) === uuid + ); + if (!matchingNFT) { + console.log('Could not find matching NFT by URI, trying by NFTokenID'); + // when first creating a vault, the tokenID is the UUID + matchingNFT = nfts.result.account_nfts.find(nft => nft.NFTokenID === uuid); + if (matchingNFT) { + console.log('Found matching NFT by NFTokenID'); + } + } else { + console.log('Found matching NFT by URI'); + } + 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): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + console.log(`Burning Ripple Vault, vault: ${nftUUID}`); + const nftTokenId = await this.getNFTokenIdForVault(nftUUID); + const burnTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenBurn', + Account: this.demo_wallet.classicAddress, + NFTokenID: nftTokenId, + }; + const burnTx: xrpl.TxResponse = await this.client.submitAndWait( + burnTransactionJson, + { wallet: this.demo_wallet } + ); + const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; + if (burnMeta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not burn temporary Ripple Vault: ${burnMeta!.TransactionResult}` + ); + } + } + + async mintNFT(vault: RawVault): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + console.log(`Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`); + const newURI = encodeNftURI(vault); + const mintTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenMint', + Account: this.demo_wallet.classicAddress, + URI: newURI, + NFTokenTaxon: 0, + }; + const mintTx: xrpl.TxResponse = await this.client.submitAndWait( + mintTransactionJson, + { wallet: this.demo_wallet } + ); + const meta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata; + if (meta!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError(`Could not mint Ripple Vault: ${meta!.TransactionResult}`); + } + return meta!.nftoken_id!; + } +} diff --git a/yarn.lock b/yarn.lock index 8aa3761..3460762 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1105,6 +1105,13 @@ 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" @@ -1117,6 +1124,11 @@ 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" @@ -1153,6 +1165,28 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== +"@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.1": version "1.3.2" resolved "https://registry.yarnpkg.com/@scure/btc-signer/-/btc-signer-1.3.2.tgz#56cf02a2e318097b1e4f531fac8ef114bdf4ddc8" @@ -1457,6 +1491,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 +1745,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" @@ -2547,6 +2603,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" @@ -4354,6 +4415,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" @@ -5024,6 +5111,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" From b12c077003a9637178488f572f3efdc1a824dd1b Mon Sep 17 00:00:00 2001 From: sosaucily Date: Mon, 30 Sep 2024 16:05:29 +0200 Subject: [PATCH 02/27] remove some noisy debug statements; --- src/network-handlers/ripple-handler.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 7f35684..d47d012 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -49,7 +49,6 @@ function decodeNftURI(URI: string): RawVault { try { VERSION = parseInt(URI.slice(0, 2), 16); status = parseInt(URI.slice(2, 4), 16); - console.log(`Decoding URI: ${URI}`); uuid = URI.slice(4, 68); valueLocked = BigNumber.from(`0x${URI.slice(68, 84)}`); valueMinted = BigNumber.from(`0x${URI.slice(84, 100)}`); @@ -70,9 +69,9 @@ function decodeNftURI(URI: string): RawVault { console.log(`URI which failed to Decode: ${URI}`); return {} as RawVault; } - console.log( - `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` - ); + // console.log( + // `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` + // ); const baseVault = buildDefaultNftVault(); return { ...baseVault, From f00241c40850198f563fa1bc5d62d5f4c66ed67d Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 30 Sep 2024 16:06:17 +0200 Subject: [PATCH 03/27] feat: add check functions to ripplehandler --- src/network-handlers/ripple-handler.ts | 84 +++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 7f35684..1ed0f87 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -1,16 +1,27 @@ import { BigNumber } from 'ethers'; -import xrpl, { AccountNFTsRequest, SubmittableTransaction } from 'xrpl'; +import xrpl, { + AccountNFTsRequest, + CheckCash, + CheckCreate, + SubmittableTransaction, + TxResponse, +} from 'xrpl'; import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; import { RippleError } from '../models/errors.js'; import { RawVault, VaultState } from '../models/ethereum-models.js'; +interface SignResponse { + tx_blob: string; + hash: string; +} + function encodeNftURI(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'); - console.log( - `UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}` - ); + // console.log( + // `UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}` + // ); let uuid = vault.uuid; if (uuid === '') { uuid = vault.uuid.padStart(64, '0'); @@ -70,9 +81,9 @@ function decodeNftURI(URI: string): RawVault { console.log(`URI which failed to Decode: ${URI}`); return {} as RawVault; } - console.log( - `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` - ); + // console.log( + // `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` + // ); const baseVault = buildDefaultNftVault(); return { ...baseVault, @@ -466,4 +477,63 @@ export class RippleHandler { } return meta!.nftoken_id!; } + + async createCheck(xrplDestinationAddress: string, dlcBTCAmount: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + + const createCheckTransactionJSON: CheckCreate = { + TransactionType: 'CheckCreate', + Account: this.demo_wallet.classicAddress, + Destination: xrplDestinationAddress, + SendMax: { + currency: 'DLC', + value: dlcBTCAmount, + issuer: xrplDestinationAddress, + }, + }; + + const updatedCreateCheckTransactionJSON: CheckCreate = await this.client.autofill( + createCheckTransactionJSON + ); + + const signCreateCheckTransactionResponse: SignResponse = this.demo_wallet.sign( + updatedCreateCheckTransactionJSON + ); + + const submitCreateCheckTransactionResponse: TxResponse = + await this.client.submitAndWait(signCreateCheckTransactionResponse.tx_blob); + + return submitCreateCheckTransactionResponse.result.hash; + } + + async cashCheck(checkID: string, dlcBTCAmount: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + + const cashCheckTransactionJSON: CheckCash = { + TransactionType: 'CheckCash', + Account: this.demo_wallet.classicAddress, + CheckID: checkID, + Amount: { + currency: 'DLC', + value: dlcBTCAmount, + issuer: this.demo_wallet.classicAddress, + }, + }; + + const updatedCashCheckTransactionJSON: CheckCash = + await this.client.autofill(cashCheckTransactionJSON); + + const signCashCheckTransactionResponse: SignResponse = this.demo_wallet.sign( + updatedCashCheckTransactionJSON + ); + + const submitCashCheckTransactionResponse: TxResponse = + await this.client.submitAndWait(signCashCheckTransactionResponse.tx_blob); + + return submitCashCheckTransactionResponse.result.hash; + } } From 09fe59b1ad745d6158164884860f2c405600568c Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 2 Oct 2024 16:28:28 +0200 Subject: [PATCH 04/27] feat: modify ripple handler to handle token minting --- .gitignore | 3 + src/network-handlers/ripple-handler.ts | 193 ++++++++++++++++++++----- 2 files changed, 163 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 2b3f55c..a138e33 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Local Netlify folder +.netlify diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index ae5bc2f..f1c1e28 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -1,8 +1,15 @@ +import { Decimal } from 'decimal.js'; import { BigNumber } from 'ethers'; import xrpl, { AccountNFTsRequest, + AccountObject, + AccountObjectsResponse, CheckCash, CheckCreate, + IssuedCurrencyAmount, + LedgerEntry, + Payment, + Request, SubmittableTransaction, TxResponse, } from 'xrpl'; @@ -10,6 +17,7 @@ import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMi import { RippleError } from '../models/errors.js'; import { RawVault, VaultState } from '../models/ethereum-models.js'; +import { shiftValue, unshiftValue } from '../utilities/index.js'; interface SignResponse { tx_blob: string; @@ -36,7 +44,7 @@ function encodeNftURI(vault: RawVault): string { const wdTxId = vault.wdTxId.padStart(64, '0'); const btcMintFeeBasisPoints = vault.btcMintFeeBasisPoints._hex.substring(2).padStart(2, '0'); const btcRedeemFeeBasisPoints = vault.btcRedeemFeeBasisPoints._hex.substring(2).padStart(2, '0'); - const btcFeeRecipient = vault.btcFeeRecipient.padStart(64, '0'); + const btcFeeRecipient = vault.btcFeeRecipient.padStart(66, '0'); const taprootPubKey = vault.taprootPubKey.padStart(64, '0'); console.log( 'built URI:', @@ -90,6 +98,7 @@ function decodeNftURI(URI: string): RawVault { status: status, valueLocked: valueLocked, valueMinted: valueMinted, + creator: 'rpCusJBGNdpjZ74kSrEvE2aQW9P4JrjSDq', fundingTxId: fundingTxId, wdTxId: wdTxId, btcMintFeeBasisPoints: btcMintFeeBasisPoints, @@ -117,7 +126,7 @@ function buildDefaultNftVault(): RawVault { valueMinted: BigNumber.from(0), protocolContract: '', timestamp: BigNumber.from(0), - creator: '', + creator: 'rpCusJBGNdpjZ74kSrEvE2aQW9P4JrjSDq', status: 0, fundingTxId: '0'.repeat(64), closingTxId: '', @@ -131,11 +140,13 @@ function buildDefaultNftVault(): RawVault { export class RippleHandler { private client: xrpl.Client; - private demo_wallet: xrpl.Wallet; + private customerWallet: xrpl.Wallet; + private issuerWallet: xrpl.Wallet; private constructor() { this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); - this.demo_wallet = xrpl.Wallet.fromSeed('sEdV6wSeVoUGwu7KHyFZ3UkrQxpvGZU'); //rNT2CxBbKtiUwy4UFwXS11PETZVW8j4k3g + this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo'); + this.customerWallet = xrpl.Wallet.fromSeed('sEdSyBuZd8CHSs4Gdd2bgucAYQTJvMB'); } static fromWhatever(): RippleHandler { @@ -158,7 +169,7 @@ export class RippleHandler { await this.client.connect(); } try { - return this.demo_wallet.classicAddress; + return this.customerWallet.classicAddress; } catch (error) { throw new RippleError(`Could not fetch Address Info: ${error}`); } @@ -203,17 +214,19 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.demo_wallet.classicAddress, + account: this.issuerWallet.classicAddress, }; let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); const nftTokenId = await this.getNFTokenIdForVault(nftUUID); - const matchingNFT = nfts.result.account_nfts.find(nft => nft.NFTokenID === nftTokenId); - if (!matchingNFT) { + const matchingNFT = nfts.result.account_nfts.filter(nft => nft.NFTokenID === nftTokenId); + if (matchingNFT.length === 0) { throw new RippleError(`Vault with UUID: ${nftUUID} not found`); + } else if (matchingNFT.length > 1) { + throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`); } - const matchingVault: RawVault = decodeNftURI(matchingNFT.URI!); + const matchingVault: RawVault = decodeNftURI(matchingNFT[0].URI!); return lowercaseHexFields(matchingVault); } catch (error) { throw new RippleError(`Could not fetch Vault: ${error}`); @@ -247,11 +260,12 @@ export class RippleHandler { await this.client.connect(); } try { + console.log(`Performing Withdraw for User: ${uuid}`); let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); - // return await withdraw(this.ethereumContracts.dlcManagerContract, vaultUUID, withdrawAmount); const thisVault = await this.getRawVault(nftUUID); await this.burnNFT(nftUUID); + thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount)); await this.mintNFT(thisVault); } catch (error) { @@ -282,6 +296,13 @@ export class RippleHandler { valueMinted: BigNumber.from(updatedValueMinted), valueLocked: BigNumber.from(updatedValueMinted), }; + if (updatedValueMinted > 0 && thisVault.valueMinted.toNumber() < Number(updatedValueMinted)) { + const mintValue = unshiftValue( + new Decimal(Number(updatedValueMinted)).minus(thisVault.valueMinted.toNumber()).toNumber() + ); + console.log(`Minting ${mintValue}`); + await this.mintToken(thisVault.creator, mintValue.toString()); + } await this.mintNFT(newVault); console.log(`Vault status set to FUNDED, vault: ${nftUUID}`); } catch (error) { @@ -378,7 +399,7 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.demo_wallet.classicAddress, + account: this.issuerWallet.classicAddress, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); @@ -405,7 +426,7 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.demo_wallet.classicAddress, + account: this.issuerWallet.classicAddress, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); @@ -439,13 +460,15 @@ export class RippleHandler { const nftTokenId = await this.getNFTokenIdForVault(nftUUID); const burnTransactionJson: SubmittableTransaction = { TransactionType: 'NFTokenBurn', - Account: this.demo_wallet.classicAddress, + Account: this.issuerWallet.classicAddress, NFTokenID: nftTokenId, }; const burnTx: xrpl.TxResponse = await this.client.submitAndWait( burnTransactionJson, - { wallet: this.demo_wallet } + { wallet: this.issuerWallet } ); + + console.log('burnTx:', burnTx); const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; if (burnMeta!.TransactionResult !== 'tesSUCCESS') { throw new RippleError( @@ -458,52 +481,114 @@ export class RippleHandler { if (!this.client.isConnected()) { await this.client.connect(); } - console.log(`Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`); + console.log(`Minting NFT with properties of Vault ${vault.uuid}`); + const newURI = encodeNftURI(vault); - const mintTransactionJson: SubmittableTransaction = { + + const mintNFTTransactionJSON: SubmittableTransaction = { TransactionType: 'NFTokenMint', - Account: this.demo_wallet.classicAddress, + Account: this.issuerWallet.classicAddress, URI: newURI, NFTokenTaxon: 0, }; - const mintTx: xrpl.TxResponse = await this.client.submitAndWait( - mintTransactionJson, - { wallet: this.demo_wallet } + + const mintNFTTransactionResponse: xrpl.TxResponse = + await this.client.submitAndWait(mintNFTTransactionJSON, { wallet: this.issuerWallet }); + + const mintNFTTransactionResponseMetadata = mintNFTTransactionResponse.result + .meta as NFTokenMintMetadata; + + if (mintNFTTransactionResponseMetadata!.TransactionResult !== 'tesSUCCESS') { + throw new RippleError( + `Could not mint NFT: ${mintNFTTransactionResponseMetadata.TransactionResult}` + ); + } + + if (!mintNFTTransactionResponseMetadata.nftoken_id) { + throw new RippleError('Could not find NFT Token ID in NFTokenMint response metadata'); + } + + return mintNFTTransactionResponseMetadata.nftoken_id; + } + + async getAllChecks(): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + + const getAccountObjectsRequestJSON: Request = { + command: 'account_objects', + account: this.issuerWallet.classicAddress, + ledger_index: 'validated', + type: 'check', + }; + + const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request( + getAccountObjectsRequestJSON ); - const meta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata; - if (meta!.TransactionResult !== 'tesSUCCESS') { - throw new RippleError(`Could not mint Ripple Vault: ${meta!.TransactionResult}`); + + return getAccountObjectsResponse.result.account_objects; + } + + async getAndCashAllChecksAndUpdateNFT(): Promise { + const allChecks = (await this.getAllChecks()) as LedgerEntry.Check[]; + const allVaults = await this.getContractVaults(); + + for (const check of allChecks) { + try { + const checkSendMax = check.SendMax as IssuedCurrencyAmount; + + await this.cashCheck(check.index, checkSendMax.value); + + const vault = allVaults.find( + vault => vault.uuid.toUpperCase().slice(2) === check.InvoiceID + ); + if (!vault) { + throw new RippleError( + `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` + ); + } + await this.withdraw(vault.uuid, BigInt(shiftValue(Number(checkSendMax.value)))); + } catch (error) { + console.error(`Error cashing Check: ${error}`); + } } - return meta!.nftoken_id!; } - async createCheck(xrplDestinationAddress: string, dlcBTCAmount: string): Promise { + async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { if (!this.client.isConnected()) { await this.client.connect(); } + console.log(`Creating Check for Vault ${vaultUUID} with an amount of ${dlcBTCAmount}`); const createCheckTransactionJSON: CheckCreate = { TransactionType: 'CheckCreate', - Account: this.demo_wallet.classicAddress, - Destination: xrplDestinationAddress, + Account: this.customerWallet.classicAddress, + Destination: this.issuerWallet.classicAddress, + DestinationTag: 1, SendMax: { currency: 'DLC', - value: dlcBTCAmount, - issuer: xrplDestinationAddress, + value: unshiftValue(Number(dlcBTCAmount)).toString(), + issuer: this.issuerWallet.classicAddress, }, + InvoiceID: vaultUUID.slice(2), }; const updatedCreateCheckTransactionJSON: CheckCreate = await this.client.autofill( createCheckTransactionJSON ); - const signCreateCheckTransactionResponse: SignResponse = this.demo_wallet.sign( + const signCreateCheckTransactionResponse: SignResponse = this.customerWallet.sign( updatedCreateCheckTransactionJSON ); const submitCreateCheckTransactionResponse: TxResponse = await this.client.submitAndWait(signCreateCheckTransactionResponse.tx_blob); + console.log( + `Response for submitted Create Check for Vault ${vaultUUID} request: ${JSON.stringify(submitCreateCheckTransactionResponse, null, 2)}` + ); + return submitCreateCheckTransactionResponse.result.hash; } @@ -512,27 +597,69 @@ export class RippleHandler { await this.client.connect(); } + console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`); + const cashCheckTransactionJSON: CheckCash = { TransactionType: 'CheckCash', - Account: this.demo_wallet.classicAddress, + Account: this.issuerWallet.classicAddress, CheckID: checkID, Amount: { currency: 'DLC', value: dlcBTCAmount, - issuer: this.demo_wallet.classicAddress, + issuer: this.issuerWallet.classicAddress, }, }; const updatedCashCheckTransactionJSON: CheckCash = await this.client.autofill(cashCheckTransactionJSON); - const signCashCheckTransactionResponse: SignResponse = this.demo_wallet.sign( + const signCashCheckTransactionResponse: SignResponse = this.issuerWallet.sign( updatedCashCheckTransactionJSON ); const submitCashCheckTransactionResponse: TxResponse = await this.client.submitAndWait(signCashCheckTransactionResponse.tx_blob); + console.log( + `Response for submitted Cash Check of Check ID ${checkID} request: ${JSON.stringify(submitCashCheckTransactionResponse, null, 2)}` + ); + return submitCashCheckTransactionResponse.result.hash; } + + async mintToken(xrplDestinationAddress: string, dlcBTCAmount: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + + console.log(`Minting ${dlcBTCAmount} dlcBTC to ${xrplDestinationAddress} address`); + + const sendTokenTransactionJSON: Payment = { + TransactionType: 'Payment', + Account: this.issuerWallet.classicAddress, + Destination: xrplDestinationAddress, + DestinationTag: 1, + Amount: { + currency: 'DLC', + value: dlcBTCAmount, + issuer: this.issuerWallet.classicAddress, + }, + }; + + const updatedSendTokenTransactionJSON: Payment = + await this.client.autofill(sendTokenTransactionJSON); + + const signSendTokenTransactionResponse: SignResponse = this.issuerWallet.sign( + updatedSendTokenTransactionJSON + ); + + const submitSendTokenTransactionResponse: TxResponse = + await this.client.submitAndWait(signSendTokenTransactionResponse.tx_blob); + + console.log( + `Response for submitted Payment to ${xrplDestinationAddress} address request: ${JSON.stringify(submitSendTokenTransactionResponse, null, 2)}` + ); + + return submitSendTokenTransactionResponse.result.hash; + } } From 30014a700065058484e9a71470c64a817dbc034f Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Sat, 5 Oct 2024 16:01:04 +0200 Subject: [PATCH 05/27] feat: modify customer wallet seed --- src/network-handlers/ripple-handler.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index f1c1e28..02dcfcc 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -98,7 +98,7 @@ function decodeNftURI(URI: string): RawVault { status: status, valueLocked: valueLocked, valueMinted: valueMinted, - creator: 'rpCusJBGNdpjZ74kSrEvE2aQW9P4JrjSDq', + creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', fundingTxId: fundingTxId, wdTxId: wdTxId, btcMintFeeBasisPoints: btcMintFeeBasisPoints, @@ -126,7 +126,7 @@ function buildDefaultNftVault(): RawVault { valueMinted: BigNumber.from(0), protocolContract: '', timestamp: BigNumber.from(0), - creator: 'rpCusJBGNdpjZ74kSrEvE2aQW9P4JrjSDq', + creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', status: 0, fundingTxId: '0'.repeat(64), closingTxId: '', @@ -146,7 +146,7 @@ export class RippleHandler { private constructor() { this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo'); - this.customerWallet = xrpl.Wallet.fromSeed('sEdSyBuZd8CHSs4Gdd2bgucAYQTJvMB'); + this.customerWallet = xrpl.Wallet.fromSeed('sEdSKUhR1Hhwomo7CsUzAe2pv7nqUXT'); } static fromWhatever(): RippleHandler { @@ -532,6 +532,7 @@ export class RippleHandler { async getAndCashAllChecksAndUpdateNFT(): Promise { const allChecks = (await this.getAllChecks()) as LedgerEntry.Check[]; + console.log('All Checks:', allChecks); const allVaults = await this.getContractVaults(); for (const check of allChecks) { @@ -596,6 +597,8 @@ export class RippleHandler { if (!this.client.isConnected()) { await this.client.connect(); } + if (checkID === '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA') + throw new Error('Invalid Check'); console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`); From e67a4a5feba8dd08b82851d763d5b1b45d32b288 Mon Sep 17 00:00:00 2001 From: sosaucily Date: Mon, 7 Oct 2024 12:23:52 +0200 Subject: [PATCH 06/27] Support multisig for XRPL txs --- src/models/ethereum-models.ts | 14 + src/network-handlers/ripple-handler.ts | 525 ++++++++++++++----------- 2 files changed, 319 insertions(+), 220 deletions(-) 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/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 02dcfcc..397c4a3 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -16,7 +16,7 @@ import xrpl, { import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; import { RippleError } from '../models/errors.js'; -import { RawVault, VaultState } from '../models/ethereum-models.js'; +import { RawVault, SSFVaultUpdate, SSPVaultUpdate } from '../models/ethereum-models.js'; import { shiftValue, unshiftValue } from '../utilities/index.js'; interface SignResponse { @@ -54,7 +54,7 @@ function encodeNftURI(vault: RawVault): string { } function decodeNftURI(URI: string): RawVault { - let VERSION = 0; + // let VERSION = 0; let uuid = ''; let valueLocked = BigNumber.from(0); let valueMinted = BigNumber.from(0); @@ -66,7 +66,7 @@ function decodeNftURI(URI: string): RawVault { let btcFeeRecipient = ''; let taprootPubKey = ''; try { - VERSION = parseInt(URI.slice(0, 2), 16); + // VERSION = parseInt(URI.slice(0, 2), 16); status = parseInt(URI.slice(2, 4), 16); uuid = URI.slice(4, 68); valueLocked = BigNumber.from(`0x${URI.slice(68, 84)}`); @@ -98,7 +98,7 @@ function decodeNftURI(URI: string): RawVault { status: status, valueLocked: valueLocked, valueMinted: valueMinted, - creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', + creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', //this.customerWallet.classicAddress ? fundingTxId: fundingTxId, wdTxId: wdTxId, btcMintFeeBasisPoints: btcMintFeeBasisPoints, @@ -141,16 +141,38 @@ function buildDefaultNftVault(): RawVault { export class RippleHandler { private client: xrpl.Client; private customerWallet: xrpl.Wallet; + private wallet: xrpl.Wallet; private issuerWallet: xrpl.Wallet; - private constructor() { + private constructor(seed: string) { this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); - this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo'); + this.wallet = xrpl.Wallet.fromSeed(seed); + this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo'); // address: ra9epzthPkNXykgfadCwu8D7mtajj8DVCP this.customerWallet = xrpl.Wallet.fromSeed('sEdSKUhR1Hhwomo7CsUzAe2pv7nqUXT'); } - static fromWhatever(): RippleHandler { - return new RippleHandler(); + static fromSeed(seed: string): RippleHandler { + return new RippleHandler(seed); + } + + async submit(signatures: string[]): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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 { @@ -175,38 +197,6 @@ export class RippleHandler { } } - // static fromPrivateKey( - // ethereumDeploymentPlans: EthereumDeploymentPlan[], - // ethereumPrivateKey: string, - // rpcEndpoint: string - // ): EthereumHandler { - // const provider = getProvider(rpcEndpoint); - // const signer = new Wallet(ethereumPrivateKey, provider); - // const ethereumContracts = getEthereumContracts(ethereumDeploymentPlans, signer); - // return new EthereumHandler(ethereumContracts); - // } - - // static fromSigner( - // ethereumDeploymentPlans: EthereumDeploymentPlan[], - // signer: providers.JsonRpcSigner - // ): EthereumHandler { - // const ethereumContracts = getEthereumContracts(ethereumDeploymentPlans, signer); - // return new EthereumHandler(ethereumContracts); - // } - - // getContracts(): DLCEthereumContracts { - // return this.ethereumContracts; - // } - - // async getAllUserVaults(): Promise { - // try { - // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); - // return await getAllAddressVaults(this.ethereumContracts.dlcManagerContract, userAddress); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch all User Vaults: ${error}`); - // } - // } - async getRawVault(uuid: string): Promise { if (!this.client.isConnected()) { await this.client.connect(); @@ -233,26 +223,20 @@ export class RippleHandler { } } - async setupVault(): Promise { + async setupVault(uuid: string): Promise { if (!this.client.isConnected()) { await this.client.connect(); } try { const newVault = buildDefaultNftVault(); - const newVaultUUID = await this.mintNFT(newVault); - - await this.burnNFT(newVaultUUID); - - newVault.uuid = newVaultUUID; - await this.mintNFT(newVault); - - return `0x${newVaultUUID}`; + newVault.uuid = uuid; + return await this.mintNFT(newVault); } catch (error) { throw new RippleError(`Could not setup Ripple Vault: ${error}`); } } - async withdraw(uuid: string, withdrawAmount: bigint) { + async withdraw(uuid: string, withdrawAmount: bigint): Promise { // Things like withdraw and deposit should get the existing NFT vault // then burn the NFT, and mint a new one with the updated value // putting the UUID into the URI @@ -264,133 +248,157 @@ export class RippleHandler { let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); const thisVault = await this.getRawVault(nftUUID); - await this.burnNFT(nftUUID); + const burnSig = await this.burnNFT(nftUUID, 1); thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount)); - await this.mintNFT(thisVault); + const mintSig = await this.mintNFT(thisVault, 2); + return [burnSig, mintSig]; } catch (error) { throw new RippleError(`Unable to perform Withdraw for User: ${error}`); } } async setVaultStatusFunded( - uuid: string, - bitcoinTransactionID: string, - updatedValueMinted: bigint + burnNFTSignedTxBlobs: string[], + mintTokensSignedTxBlobs: string[], // this can be a set of empty string is no tokens are being minted + mintNFTSignedTxBlobs: string[] ): Promise { if (!this.client.isConnected()) { await this.client.connect(); } try { - let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; - nftUUID = nftUUID.toUpperCase(); + 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}` + ); + } - console.log(`Setting Vault status to FUNDED, vault: ${nftUUID}`); - const thisVault = await this.getRawVault(nftUUID); - await this.burnNFT(nftUUID); - const newVault = { - ...thisVault, - status: VaultState.FUNDED, - fundingTxId: bitcoinTransactionID, - wdTxId: '', - valueMinted: BigNumber.from(updatedValueMinted), - valueLocked: BigNumber.from(updatedValueMinted), - }; - if (updatedValueMinted > 0 && thisVault.valueMinted.toNumber() < Number(updatedValueMinted)) { - const mintValue = unshiftValue( - new Decimal(Number(updatedValueMinted)).minus(thisVault.valueMinted.toNumber()).toNumber() + // 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}` ); - console.log(`Minting ${mintValue}`); - await this.mintToken(thisVault.creator, mintValue.toString()); } - await this.mintNFT(newVault); - console.log(`Vault status set to FUNDED, vault: ${nftUUID}`); } catch (error) { throw new RippleError(`Unable to set Vault status to FUNDED: ${error}`); } } - async setVaultStatusPending( - uuid: string, - bitcoinTransactionID: string, - updatedValueMinted: bigint, - userPubkey: string + async performCheckCashAndNftUpdate( + cashCheckSignedTxBlobs: string[], + burnNFTSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] ): Promise { if (!this.client.isConnected()) { await this.client.connect(); } try { - let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; - nftUUID = nftUUID.toUpperCase(); + 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(`Setting Vault status to Pending, vault: ${nftUUID}`); - const thisVault = await this.getRawVault(nftUUID); - await this.burnNFT(nftUUID); - const newVault = { - ...thisVault, - status: VaultState.PENDING, - wdTxId: bitcoinTransactionID, - taprootPubKey: userPubkey, - }; - await this.mintNFT(newVault); - console.log(`Vault status set to Pending, vault: ${nftUUID}`); + 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 FUNDED: ${error}`); + throw new RippleError(`Unable to set Vault status to PENDING: ${error}`); } } - // async getUserDLCBTCBalance(): Promise { - // try { - // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); - // return await getAddressDLCBTCBalance(this.ethereumContracts.dlcBTCContract, userAddress); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch User's dlcBTC balance: ${error}`); - // } - // } - - // async getDLCBTCTotalSupply(): Promise { - // try { - // return await getDLCBTCTotalSupply(this.ethereumContracts.dlcBTCContract); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch Total Supply of dlcBTC: ${error}`); - // } - // } - - // async getLockedBTCBalance(userVaults?: RawVault[]): Promise { - // try { - // if (!userVaults) { - // userVaults = await this.getAllUserVaults(); - // } - // return await getLockedBTCBalance(userVaults); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch Total Supply of Locked dlcBTC: ${error}`); - // } - // } - - // async getAttestorGroupPublicKey(): Promise { - // try { - // return getAttestorGroupPublicKey(this.ethereumContracts.dlcManagerContract); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch Attestor Public Key: ${error}`); - // } - // } - - // async isWhiteLisingEnabled(): Promise { - // try { - // return await isWhitelistingEnabled(this.ethereumContracts.dlcManagerContract); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch Whitelisting Status: ${error}`); - // } - // } - - // async isUserWhitelisted(): Promise { - // try { - // const userAddress = await this.ethereumContracts.dlcManagerContract.signer.getAddress(); - // return await isUserWhitelisted(this.ethereumContracts.dlcManagerContract, userAddress); - // } catch (error) { - // throw new EthereumHandlerError(`Could not fetch User Whitelisting Status: ${error}`); - // } - // } + async setVaultStatusPending( + burnNFTSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] + ): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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 { if (!this.client.isConnected()) { @@ -406,12 +414,6 @@ export class RippleHandler { const allNFTs = nfts.result.account_nfts; const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeNftURI(nft.URI!))); - // allVaults.forEach((vault, index) => { - // if (vault.uuid === '') { - // vault.uuid = allNFTs[index].NFTokenID; - // } - // }); - return allVaults; } catch (error) { throw new RippleError(`Could not fetch All Vaults: ${error}`); @@ -430,19 +432,10 @@ export class RippleHandler { }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - let matchingNFT = nfts.result.account_nfts.find( + const matchingNFT = nfts.result.account_nfts.find( nft => decodeNftURI(nft.URI!).uuid.slice(2) === uuid ); - if (!matchingNFT) { - console.log('Could not find matching NFT by URI, trying by NFTokenID'); - // when first creating a vault, the tokenID is the UUID - matchingNFT = nfts.result.account_nfts.find(nft => nft.NFTokenID === uuid); - if (matchingNFT) { - console.log('Found matching NFT by NFTokenID'); - } - } else { - console.log('Found matching NFT by URI'); - } + if (!matchingNFT) { throw new RippleError(`Vault for uuid: ${uuid} not found`); } @@ -452,63 +445,110 @@ export class RippleHandler { } } - async burnNFT(nftUUID: string): Promise { + async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { if (!this.client.isConnected()) { await this.client.connect(); } - console.log(`Burning Ripple Vault, vault: ${nftUUID}`); + console.log(`Getting sig for Burning Ripple Vault, vault: ${nftUUID}`); const nftTokenId = await this.getNFTokenIdForVault(nftUUID); const burnTransactionJson: SubmittableTransaction = { TransactionType: 'NFTokenBurn', Account: this.issuerWallet.classicAddress, NFTokenID: nftTokenId, }; - const burnTx: xrpl.TxResponse = await this.client.submitAndWait( - burnTransactionJson, - { wallet: this.issuerWallet } - ); + const preparedBurnTx = await this.client.autofill(burnTransactionJson, 3); // this hardcoded number should match the number of active signers - console.log('burnTx:', burnTx); - const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata; - if (burnMeta!.TransactionResult !== 'tesSUCCESS') { - throw new RippleError( - `Could not burn temporary Ripple Vault: ${burnMeta!.TransactionResult}` - ); + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + preparedBurnTx.LastLedgerSequence = + Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? + + 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; } - async mintNFT(vault: RawVault): Promise { + async mintNFT(vault: RawVault, incrementBy: number = 0): Promise { if (!this.client.isConnected()) { await this.client.connect(); } - console.log(`Minting NFT with properties of Vault ${vault.uuid}`); - + console.log(`Getting sig for Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`); const newURI = encodeNftURI(vault); - - const mintNFTTransactionJSON: SubmittableTransaction = { + console.log('newURI: ', newURI); + const mintTransactionJson: SubmittableTransaction = { TransactionType: 'NFTokenMint', Account: this.issuerWallet.classicAddress, URI: newURI, NFTokenTaxon: 0, }; + const preparedMintTx = await this.client.autofill(mintTransactionJson, 3); + + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + preparedMintTx.LastLedgerSequence = + Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; + if (incrementBy > 0) { + preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; + } - const mintNFTTransactionResponse: xrpl.TxResponse = - await this.client.submitAndWait(mintNFTTransactionJSON, { wallet: this.issuerWallet }); + console.log('preparedMintTx ', preparedMintTx); - const mintNFTTransactionResponseMetadata = mintNFTTransactionResponse.result - .meta as NFTokenMintMetadata; + const sig = this.wallet.sign(preparedMintTx, true); + console.log('tx_one_sig: ', sig); + return sig.tx_blob; + } - if (mintNFTTransactionResponseMetadata!.TransactionResult !== 'tesSUCCESS') { - throw new RippleError( - `Could not mint NFT: ${mintNFTTransactionResponseMetadata.TransactionResult}` - ); + async getSigUpdateVaultForSSP(uuid: string, updates: SSPVaultUpdate): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); } - - if (!mintNFTTransactionResponseMetadata.nftoken_id) { - throw new RippleError('Could not find NFT Token ID in NFTokenMint response metadata'); + 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}`); } + } - return mintNFTTransactionResponseMetadata.nftoken_id; + async getSigUpdateVaultForSSF( + uuid: string, + updates: SSFVaultUpdate, + updateSequenceBy: number + ): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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 { @@ -530,16 +570,16 @@ export class RippleHandler { return getAccountObjectsResponse.result.account_objects; } - async getAndCashAllChecksAndUpdateNFT(): Promise { + async getAndCashAllChecksAndUpdateNFT(): Promise { const allChecks = (await this.getAllChecks()) as LedgerEntry.Check[]; - console.log('All Checks:', allChecks); + // console.log('All Checks:', allChecks); const allVaults = await this.getContractVaults(); for (const check of allChecks) { try { const checkSendMax = check.SendMax as IssuedCurrencyAmount; - await this.cashCheck(check.index, checkSendMax.value); + const my_check_cash_sig = await this.cashCheck(check.index, checkSendMax.value); const vault = allVaults.find( vault => vault.uuid.toUpperCase().slice(2) === check.InvoiceID @@ -549,11 +589,16 @@ export class RippleHandler { `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` ); } - await this.withdraw(vault.uuid, BigInt(shiftValue(Number(checkSendMax.value)))); + const two_more_sigs = await this.withdraw( + vault.uuid, + BigInt(shiftValue(Number(checkSendMax.value))) + ); + return [my_check_cash_sig, ...two_more_sigs]; } catch (error) { - console.error(`Error cashing Check: ${error}`); + console.error(`Error cashing Check: ${error} \n continuing`); } } + return []; } async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { @@ -572,13 +617,18 @@ export class RippleHandler { value: unshiftValue(Number(dlcBTCAmount)).toString(), issuer: this.issuerWallet.classicAddress, }, - InvoiceID: vaultUUID.slice(2), + InvoiceID: vaultUUID, }; const updatedCreateCheckTransactionJSON: CheckCreate = await this.client.autofill( createCheckTransactionJSON ); + console.log( + 'Customer is about to sign the following sendCheck tx: ', + updatedCreateCheckTransactionJSON + ); + const signCreateCheckTransactionResponse: SignResponse = this.customerWallet.sign( updatedCreateCheckTransactionJSON ); @@ -597,7 +647,15 @@ export class RippleHandler { if (!this.client.isConnected()) { await this.client.connect(); } - if (checkID === '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA') + + //what's + if ( + [ + '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA', + '93BAA031806AE4902933C1EE9B66E7EBAF0F7A182314085BEFF99DF080A1CBCB', + 'F51C7E3CCFD2EC8CA9A460A34C5BC185E9466031865E76736C0A60BC3F7C7316', + ].includes(checkID) + ) throw new Error('Invalid Check'); console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`); @@ -613,34 +671,50 @@ export class RippleHandler { }, }; - const updatedCashCheckTransactionJSON: CheckCash = - await this.client.autofill(cashCheckTransactionJSON); - - const signCashCheckTransactionResponse: SignResponse = this.issuerWallet.sign( - updatedCashCheckTransactionJSON + const updatedCashCheckTransactionJSON: CheckCash = await this.client.autofill( + cashCheckTransactionJSON, + 3 // hardcoded? not good? just fee related? ); - const submitCashCheckTransactionResponse: TxResponse = - await this.client.submitAndWait(signCashCheckTransactionResponse.tx_blob); + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + updatedCashCheckTransactionJSON.LastLedgerSequence = + Math.ceil(updatedCashCheckTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; console.log( - `Response for submitted Cash Check of Check ID ${checkID} request: ${JSON.stringify(submitCashCheckTransactionResponse, null, 2)}` + 'Issuer is about to sign the following cashCheck tx: ', + updatedCashCheckTransactionJSON ); - return submitCashCheckTransactionResponse.result.hash; + const signCashCheckTransactionSig: SignResponse = this.wallet.sign( + updatedCashCheckTransactionJSON, + true + ); + + return signCashCheckTransactionSig.tx_blob; } - async mintToken(xrplDestinationAddress: string, dlcBTCAmount: string): Promise { + async mintTokens( + updatedValueMinted: number, + valueMinted: number, + incrementBy: number = 0 + ): Promise { if (!this.client.isConnected()) { await this.client.connect(); } - console.log(`Minting ${dlcBTCAmount} dlcBTC to ${xrplDestinationAddress} address`); + 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 ${this.customerWallet.classicAddress} address`); const sendTokenTransactionJSON: Payment = { TransactionType: 'Payment', Account: this.issuerWallet.classicAddress, - Destination: xrplDestinationAddress, + Destination: this.customerWallet.classicAddress, DestinationTag: 1, Amount: { currency: 'DLC', @@ -649,20 +723,31 @@ export class RippleHandler { }, }; - const updatedSendTokenTransactionJSON: Payment = - await this.client.autofill(sendTokenTransactionJSON); - - const signSendTokenTransactionResponse: SignResponse = this.issuerWallet.sign( - updatedSendTokenTransactionJSON + const updatedSendTokenTransactionJSON: Payment = await this.client.autofill( + sendTokenTransactionJSON, + 3 ); - const submitSendTokenTransactionResponse: TxResponse = - await this.client.submitAndWait(signSendTokenTransactionResponse.tx_blob); + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + updatedSendTokenTransactionJSON.LastLedgerSequence = + Math.ceil(updatedSendTokenTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; + + if (incrementBy > 0) { + updatedSendTokenTransactionJSON.Sequence = + updatedSendTokenTransactionJSON.Sequence! + incrementBy; + } console.log( - `Response for submitted Payment to ${xrplDestinationAddress} address request: ${JSON.stringify(submitSendTokenTransactionResponse, null, 2)}` + 'Issuer is about to sign the following mintTokens tx: ', + updatedSendTokenTransactionJSON + ); + + const signSendTokenTransactionResponse: SignResponse = this.wallet.sign( + updatedSendTokenTransactionJSON, + true ); - return submitSendTokenTransactionResponse.result.hash; + return signSendTokenTransactionResponse.tx_blob; } } From 8b71cb67ffc714d2b682faec88aff463a652cde8 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 10 Oct 2024 12:10:59 +0200 Subject: [PATCH 07/27] feat: abstract xrpl functions, modifiy ripple handler --- package.json | 35 +-- src/constants/ripple.constants.ts | 1 + .../attestor/attestor-request.functions.ts | 14 +- src/functions/request/request.functions.ts | 17 +- src/functions/ripple/index.ts | 1 + src/functions/ripple/ripple.functions.ts | 258 ++++++++++++++++++ src/models/index.ts | 2 + src/models/ripple.model.ts | 4 + src/network-handlers/ripple-handler.ts | 181 ++---------- yarn.lock | 108 +++++--- 10 files changed, 415 insertions(+), 206 deletions(-) create mode 100644 src/constants/ripple.constants.ts create mode 100644 src/functions/ripple/index.ts create mode 100644 src/functions/ripple/ripple.functions.ts create mode 100644 src/models/ripple.model.ts diff --git a/package.json b/package.json index e099081..0b4bbd5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.2.7", + "version": "2.3.0", "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,21 +59,21 @@ "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", + "@ledgerhq/hw-app-btc": "10.4.1", + "@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", - "xrpl": "^4.0.0" + "ledger-bitcoin": "0.2.3", + "prompts": "2.4.2", + "ramda": "0.30.1", + "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..e676c76 --- /dev/null +++ b/src/constants/ripple.constants.ts @@ -0,0 +1 @@ +export const TRANSACTION_SUCCESS_CODE = 'tesSUCCESS'; diff --git a/src/functions/attestor/attestor-request.functions.ts b/src/functions/attestor/attestor-request.functions.ts index 64992bc..b3b731f 100644 --- a/src/functions/attestor/attestor-request.functions.ts +++ b/src/functions/attestor/attestor-request.functions.ts @@ -5,7 +5,19 @@ import { 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 +): Promise { + const requestBody = JSON.stringify({ user_xrpl_address: userXRPLAddress }); + return sendRequest(`${coordinatorURL}/app/setup-xrpl-vault`, 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/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..07461b1 --- /dev/null +++ b/src/functions/ripple/ripple.functions.ts @@ -0,0 +1,258 @@ +import { Decimal } from 'decimal.js'; +import { BigNumber } from 'ethers'; +import { + AccountNFToken, + AccountNFTsRequest, + CheckCreate, + Client, + SubmittableTransaction, + TransactionMetadataBase, + TxResponse, + Wallet, + convertHexToString, + convertStringToHex, +} from 'xrpl'; + +import { TRANSACTION_SUCCESS_CODE } 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 { unshiftValue } from '../../utilities/index.js'; + +function hexFieldsToLowercase(vault: RawVault): RawVault { + return { + ...vault, + creator: vault.creator.toLowerCase(), + protocolContract: vault.protocolContract.toLowerCase(), + 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; + } + await rippleClient.connect(); +} + +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 getRippleVault( + rippleClient: Client, + issuerAddress: string, + vaultUUID: string +): Promise { + try { + await connectRippleClient(rippleClient); + + let formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID.slice(2) : vaultUUID; + formattedUUID = formattedUUID.toUpperCase(); + const getAccountNFTsRequest: AccountNFTsRequest = { + command: 'account_nfts', + account: issuerAddress, + }; + + const { + result: { account_nfts: rippleNFTs }, + } = await rippleClient.request(getAccountNFTsRequest); + + const nftID = findNFTByUUID(rippleNFTs, formattedUUID).NFTokenID; + + const matchingNFTs = rippleNFTs.filter(nft => nft.NFTokenID === nftID); + + if (matchingNFTs.length === 0) { + throw new RippleError(`Vault with UUID: ${formattedUUID} not found`); + } + + if (matchingNFTs.length > 1) { + throw new RippleError(`Multiple Vaults found with UUID: ${formattedUUID}`); + } + + return hexFieldsToLowercase(decodeURI(matchingNFTs[0].URI!)); + } catch (error) { + throw new RippleError(`Error getting Vault ${vaultUUID}: ${error}`); + } +} + +export async function getAllRippleVaults( + rippleClient: Client, + issuerAddress: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const getAccountNFTsRequest: AccountNFTsRequest = { + command: 'account_nfts', + account: issuerAddress, + }; + + const { + result: { account_nfts: rippleNFTs }, + } = await rippleClient.request(getAccountNFTsRequest); + + return rippleNFTs.map(nft => hexFieldsToLowercase(decodeURI(nft.URI!))); + } catch (error) { + throw new RippleError(`Error getting Vaults: ${error}`); + } +} + +export async function createCheck( + rippleClient: Client, + rippleWallet: Wallet, + 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: rippleWallet.classicAddress, + Destination: destinationAddress, + DestinationTag: destinationTag, + SendMax: { + currency: 'DLC', + value: shiftedAmountAsNumber.toString(), + issuer: destinationAddress, + }, + InvoiceID: vaultUUID, + }; + + const updatedCreateCheckRequestJSON: CheckCreate = + await rippleClient.autofill(createCheckRequestJSON); + + console.log(`Signing Create Check for Vault ${vaultUUID}:`, updatedCreateCheckRequestJSON); + + const signCreateCheckResponse: SignResponse = rippleWallet.sign(updatedCreateCheckRequestJSON); + + const submitCreateCheckResponse: TxResponse = + await rippleClient.submitAndWait(signCreateCheckResponse.tx_blob); + + console.log( + `Response for submitted Create Check for Vault ${vaultUUID} request:`, + submitCreateCheckResponse + ); + + checkRippleTransactionResult(submitCreateCheckResponse); + + return submitCreateCheckResponse.result.hash; + } catch (error) { + throw new RippleError(`Error creating Check for Vault ${vaultUUID}: ${error}`); + } +} 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 index 397c4a3..0cd49f1 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -5,16 +5,15 @@ import xrpl, { AccountObject, AccountObjectsResponse, CheckCash, - CheckCreate, IssuedCurrencyAmount, LedgerEntry, Payment, Request, SubmittableTransaction, - TxResponse, } from 'xrpl'; import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; +import { decodeURI, encodeURI } 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'; @@ -24,90 +23,6 @@ interface SignResponse { hash: string; } -function encodeNftURI(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'); - // console.log( - // `UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}` - // ); - let uuid = vault.uuid; - if (uuid === '') { - uuid = vault.uuid.padStart(64, '0'); - } - if (uuid.substring(0, 2) === '0x') { - uuid = uuid.slice(2); - } - // add padding so valueLocked and valueMinted are always 16 characters - const valueLockedPadded = vault.valueLocked._hex.substring(2).padStart(16, '0'); - const valueMintedPadded = vault.valueMinted._hex.substring(2).padStart(16, '0'); - const fundingTxId = vault.fundingTxId.padStart(64, '0'); - const wdTxId = vault.wdTxId.padStart(64, '0'); - const btcMintFeeBasisPoints = vault.btcMintFeeBasisPoints._hex.substring(2).padStart(2, '0'); - const btcRedeemFeeBasisPoints = vault.btcRedeemFeeBasisPoints._hex.substring(2).padStart(2, '0'); - const btcFeeRecipient = vault.btcFeeRecipient.padStart(66, '0'); - const taprootPubKey = vault.taprootPubKey.padStart(64, '0'); - console.log( - 'built URI:', - `${VERSION}${status}${uuid}${valueLockedPadded}${valueMintedPadded}${fundingTxId}${wdTxId}${btcMintFeeBasisPoints}${btcRedeemFeeBasisPoints}${btcFeeRecipient}${taprootPubKey}` - ); - return `${VERSION}${status}${uuid}${valueLockedPadded}${valueMintedPadded}${fundingTxId}${wdTxId}${btcMintFeeBasisPoints}${btcRedeemFeeBasisPoints}${btcFeeRecipient}${taprootPubKey}`; -} - -function decodeNftURI(URI: string): RawVault { - // let VERSION = 0; - let uuid = ''; - let valueLocked = BigNumber.from(0); - let valueMinted = BigNumber.from(0); - let status = 0; - let fundingTxId = ''; - let wdTxId = ''; - let btcMintFeeBasisPoints = BigNumber.from(0); - let btcRedeemFeeBasisPoints = BigNumber.from(0); - let btcFeeRecipient = ''; - let taprootPubKey = ''; - try { - // VERSION = parseInt(URI.slice(0, 2), 16); - status = parseInt(URI.slice(2, 4), 16); - uuid = URI.slice(4, 68); - valueLocked = BigNumber.from(`0x${URI.slice(68, 84)}`); - valueMinted = BigNumber.from(`0x${URI.slice(84, 100)}`); - fundingTxId = URI.slice(100, 164); - if (fundingTxId === '0'.repeat(64)) { - fundingTxId = ''; - } - wdTxId = URI.slice(164, 228); - if (wdTxId === '0'.repeat(64)) { - wdTxId = ''; - } - btcMintFeeBasisPoints = BigNumber.from(`0x${URI.slice(228, 230)}`); - btcRedeemFeeBasisPoints = BigNumber.from(`0x${URI.slice(230, 232)}`); - btcFeeRecipient = URI.slice(232, 298); - taprootPubKey = URI.slice(298, 362); - } catch (error) { - console.log(`Error decoding URI: ${error}`); - console.log(`URI which failed to Decode: ${URI}`); - return {} as RawVault; - } - // console.log( - // `Decoded URI: Version: ${VERSION}, status: ${status}, UUID: ${uuid}, valueLocked: ${valueLocked}, valueMinted: ${valueMinted}, fundingTxId: ${fundingTxId}, wdTxId: ${wdTxId}, btcMintFeeBasisPoints: ${btcMintFeeBasisPoints}, btcRedeemFeeBasisPoints: ${btcRedeemFeeBasisPoints}, btcFeeRecipient: ${btcFeeRecipient} , taprootPubKey: ${taprootPubKey}` - // ); - const baseVault = buildDefaultNftVault(); - return { - ...baseVault, - uuid: `0x${uuid}`, - status: status, - valueLocked: valueLocked, - valueMinted: valueMinted, - creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG', //this.customerWallet.classicAddress ? - fundingTxId: fundingTxId, - wdTxId: wdTxId, - btcMintFeeBasisPoints: btcMintFeeBasisPoints, - btcRedeemFeeBasisPoints: btcRedeemFeeBasisPoints, - btcFeeRecipient: btcFeeRecipient, - taprootPubKey: taprootPubKey, - }; -} - function lowercaseHexFields(vault: RawVault): RawVault { return { ...vault, @@ -140,19 +55,17 @@ function buildDefaultNftVault(): RawVault { export class RippleHandler { private client: xrpl.Client; - private customerWallet: xrpl.Wallet; private wallet: xrpl.Wallet; - private issuerWallet: xrpl.Wallet; + private issuerAddress: string; - private constructor(seed: string) { + private constructor(seedPhrase: string, issuerAddress: string) { this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); - this.wallet = xrpl.Wallet.fromSeed(seed); - this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo'); // address: ra9epzthPkNXykgfadCwu8D7mtajj8DVCP - this.customerWallet = xrpl.Wallet.fromSeed('sEdSKUhR1Hhwomo7CsUzAe2pv7nqUXT'); + this.wallet = xrpl.Wallet.fromSeed(seedPhrase); + this.issuerAddress = issuerAddress; } - static fromSeed(seed: string): RippleHandler { - return new RippleHandler(seed); + static fromSeed(seedPhrase: string, issuerAddress: string): RippleHandler { + return new RippleHandler(seedPhrase, issuerAddress); } async submit(signatures: string[]): Promise { @@ -191,7 +104,7 @@ export class RippleHandler { await this.client.connect(); } try { - return this.customerWallet.classicAddress; + return this.wallet.classicAddress; } catch (error) { throw new RippleError(`Could not fetch Address Info: ${error}`); } @@ -204,7 +117,7 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.issuerWallet.classicAddress, + account: this.issuerAddress, }; let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); @@ -216,20 +129,21 @@ export class RippleHandler { } else if (matchingNFT.length > 1) { throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`); } - const matchingVault: RawVault = decodeNftURI(matchingNFT[0].URI!); + const matchingVault: RawVault = decodeURI(matchingNFT[0].URI!); return lowercaseHexFields(matchingVault); } catch (error) { throw new RippleError(`Could not fetch Vault: ${error}`); } } - async setupVault(uuid: string): Promise { + async setupVault(uuid: string, userAddress: string): Promise { if (!this.client.isConnected()) { await this.client.connect(); } try { const newVault = buildDefaultNftVault(); newVault.uuid = uuid; + newVault.creator = userAddress; return await this.mintNFT(newVault); } catch (error) { throw new RippleError(`Could not setup Ripple Vault: ${error}`); @@ -407,12 +321,12 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.issuerWallet.classicAddress, + account: this.issuerAddress, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); const allNFTs = nfts.result.account_nfts; - const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeNftURI(nft.URI!))); + const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeURI(nft.URI!))); return allVaults; } catch (error) { @@ -428,12 +342,12 @@ export class RippleHandler { try { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', - account: this.issuerWallet.classicAddress, + account: this.issuerAddress, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); const matchingNFT = nfts.result.account_nfts.find( - nft => decodeNftURI(nft.URI!).uuid.slice(2) === uuid + nft => decodeURI(nft.URI!).uuid.slice(2) === uuid ); if (!matchingNFT) { @@ -453,7 +367,7 @@ export class RippleHandler { const nftTokenId = await this.getNFTokenIdForVault(nftUUID); const burnTransactionJson: SubmittableTransaction = { TransactionType: 'NFTokenBurn', - Account: this.issuerWallet.classicAddress, + Account: this.issuerAddress, NFTokenID: nftTokenId, }; const preparedBurnTx = await this.client.autofill(burnTransactionJson, 3); // this hardcoded number should match the number of active signers @@ -479,11 +393,11 @@ export class RippleHandler { await this.client.connect(); } console.log(`Getting sig for Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`); - const newURI = encodeNftURI(vault); + const newURI = encodeURI(vault); console.log('newURI: ', newURI); const mintTransactionJson: SubmittableTransaction = { TransactionType: 'NFTokenMint', - Account: this.issuerWallet.classicAddress, + Account: this.issuerAddress, URI: newURI, NFTokenTaxon: 0, }; @@ -558,7 +472,7 @@ export class RippleHandler { const getAccountObjectsRequestJSON: Request = { command: 'account_objects', - account: this.issuerWallet.classicAddress, + account: this.issuerAddress, ledger_index: 'validated', type: 'check', }; @@ -601,48 +515,6 @@ export class RippleHandler { return []; } - async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } - console.log(`Creating Check for Vault ${vaultUUID} with an amount of ${dlcBTCAmount}`); - - const createCheckTransactionJSON: CheckCreate = { - TransactionType: 'CheckCreate', - Account: this.customerWallet.classicAddress, - Destination: this.issuerWallet.classicAddress, - DestinationTag: 1, - SendMax: { - currency: 'DLC', - value: unshiftValue(Number(dlcBTCAmount)).toString(), - issuer: this.issuerWallet.classicAddress, - }, - InvoiceID: vaultUUID, - }; - - const updatedCreateCheckTransactionJSON: CheckCreate = await this.client.autofill( - createCheckTransactionJSON - ); - - console.log( - 'Customer is about to sign the following sendCheck tx: ', - updatedCreateCheckTransactionJSON - ); - - const signCreateCheckTransactionResponse: SignResponse = this.customerWallet.sign( - updatedCreateCheckTransactionJSON - ); - - const submitCreateCheckTransactionResponse: TxResponse = - await this.client.submitAndWait(signCreateCheckTransactionResponse.tx_blob); - - console.log( - `Response for submitted Create Check for Vault ${vaultUUID} request: ${JSON.stringify(submitCreateCheckTransactionResponse, null, 2)}` - ); - - return submitCreateCheckTransactionResponse.result.hash; - } - async cashCheck(checkID: string, dlcBTCAmount: string): Promise { if (!this.client.isConnected()) { await this.client.connect(); @@ -662,12 +534,12 @@ export class RippleHandler { const cashCheckTransactionJSON: CheckCash = { TransactionType: 'CheckCash', - Account: this.issuerWallet.classicAddress, + Account: this.issuerAddress, CheckID: checkID, Amount: { currency: 'DLC', value: dlcBTCAmount, - issuer: this.issuerWallet.classicAddress, + issuer: this.issuerAddress, }, }; @@ -696,6 +568,7 @@ export class RippleHandler { async mintTokens( updatedValueMinted: number, + destinationAddress: string, valueMinted: number, incrementBy: number = 0 ): Promise { @@ -709,17 +582,17 @@ export class RippleHandler { } const mintValue = unshiftValue(new Decimal(updatedValueMinted).minus(valueMinted).toNumber()); const dlcBTCAmount = mintValue.toString(); - console.log(`Minting ${dlcBTCAmount} dlcBTC to ${this.customerWallet.classicAddress} address`); + console.log(`Minting ${dlcBTCAmount} dlcBTC to ${destinationAddress} address`); const sendTokenTransactionJSON: Payment = { TransactionType: 'Payment', - Account: this.issuerWallet.classicAddress, - Destination: this.customerWallet.classicAddress, + Account: this.issuerAddress, + Destination: destinationAddress, DestinationTag: 1, Amount: { currency: 'DLC', value: dlcBTCAmount, - issuer: this.issuerWallet.classicAddress, + issuer: this.issuerAddress, }, }; diff --git a/yarn.lock b/yarn.lock index 3460762..9a7a12a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1067,17 +1067,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 +1105,7 @@ tiny-secp256k1 "1.1.6" varuint-bitcoin "1.1.2" -"@ledgerhq/hw-transport@^6.20.0", "@ledgerhq/hw-transport@^6.30.6": +"@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,6 +1115,16 @@ "@ledgerhq/logs" "^6.12.0" events "^3.3.0" +"@ledgerhq/hw-transport@^6.31.2": + 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" @@ -1119,7 +1144,7 @@ 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== @@ -1160,7 +1185,12 @@ 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== @@ -1187,7 +1217,7 @@ "@noble/hashes" "~1.5.0" "@scure/base" "~1.1.8" -"@scure/btc-signer@^1.3.1": +"@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== @@ -1335,7 +1365,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== @@ -1767,6 +1797,16 @@ bip32-path@^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" @@ -1780,16 +1820,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" @@ -1807,6 +1837,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" @@ -1828,7 +1870,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== @@ -1962,6 +2004,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" @@ -1990,11 +2037,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" @@ -2241,7 +2283,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== @@ -3673,7 +3715,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== @@ -4235,7 +4277,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== @@ -4275,7 +4317,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== @@ -4482,7 +4524,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== @@ -4764,7 +4806,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== @@ -5116,7 +5158,7 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -xrpl@^4.0.0: +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== From 4d6d192cec8cf2e2c3948c1142c4198b47d67cce Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 14 Oct 2024 17:18:11 +0200 Subject: [PATCH 08/27] feat: modify ripple functions --- src/functions/attestor/index.ts | 2 + src/functions/ripple/ripple.functions.ts | 154 +++++++++++++++++++---- 2 files changed, 133 insertions(+), 23 deletions(-) diff --git a/src/functions/attestor/index.ts b/src/functions/attestor/index.ts index 8b032cd..d3b3240 100644 --- a/src/functions/attestor/index.ts +++ b/src/functions/attestor/index.ts @@ -1,4 +1,6 @@ export { submitFundingPSBT, submitWithdrawDepositPSBT, + getAttestorExtendedGroupPublicKey, + submitSetupXRPLVaultRequest, } from '../attestor/attestor-request.functions.js'; diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index 07461b1..f0017f3 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -1,12 +1,16 @@ import { Decimal } from 'decimal.js'; import { BigNumber } from 'ethers'; import { + AccountLinesRequest, + AccountLinesResponse, AccountNFToken, AccountNFTsRequest, CheckCreate, Client, SubmittableTransaction, + Transaction, TransactionMetadataBase, + TrustSet, TxResponse, Wallet, convertHexToString, @@ -17,13 +21,11 @@ import { TRANSACTION_SUCCESS_CODE } 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 { unshiftValue } from '../../utilities/index.js'; +import { shiftValue, unshiftValue } from '../../utilities/index.js'; function hexFieldsToLowercase(vault: RawVault): RawVault { return { ...vault, - creator: vault.creator.toLowerCase(), - protocolContract: vault.protocolContract.toLowerCase(), uuid: vault.uuid.toLowerCase(), fundingTxId: vault.fundingTxId.toLowerCase(), wdTxId: vault.wdTxId.toLowerCase(), @@ -146,6 +148,42 @@ export function findNFTByUUID(rippleNFTs: AccountNFToken[], vaultUUID: string): 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 === 'DLC' && line.account === issuerAddress)) { + console.log(`Trust Line already exists for Issuer: ${issuerAddress}`); + return; + } + + const trustSetTransactionRequest: TrustSet = { + TransactionType: 'TrustSet', + Account: ownerAddress, + LimitAmount: { + currency: 'DLC', + issuer: issuerAddress, + value: '10000000000', + }, + }; + + const updatedTrustSetTransactionRequest = await rippleClient.autofill(trustSetTransactionRequest); + return updatedTrustSetTransactionRequest; +} + export async function getRippleVault( rippleClient: Client, issuerAddress: string, @@ -185,7 +223,8 @@ export async function getRippleVault( export async function getAllRippleVaults( rippleClient: Client, - issuerAddress: string + issuerAddress: string, + ownerAddress?: string ): Promise { try { await connectRippleClient(rippleClient); @@ -199,20 +238,103 @@ export async function getAllRippleVaults( result: { account_nfts: rippleNFTs }, } = await rippleClient.request(getAccountNFTsRequest); - return rippleNFTs.map(nft => hexFieldsToLowercase(decodeURI(nft.URI!))); + const rippleVaults = rippleNFTs.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 createCheck( +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, rippleWallet: Wallet, + issuerAddress: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const rippleVaults = await getAllRippleVaults( + rippleClient, + issuerAddress, + rippleWallet.classicAddress + ); + + 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, + rippleWallet: Wallet, + issuerAddress: string +): Promise { + try { + await connectRippleClient(rippleClient); + + const accountNonXRPBalancesRequest: AccountLinesRequest = { + command: 'account_lines', + account: rippleWallet.classicAddress, + ledger_index: 'validated', + }; + + const { + result: { lines }, + }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); + + const dlcBTCBalance = lines.find( + line => line.currency === 'DLC' && 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 { +): Promise { try { await connectRippleClient(rippleClient); @@ -223,7 +345,7 @@ export async function createCheck( const createCheckRequestJSON: CheckCreate = { TransactionType: 'CheckCreate', - Account: rippleWallet.classicAddress, + Account: ownerAddress, Destination: destinationAddress, DestinationTag: destinationTag, SendMax: { @@ -237,21 +359,7 @@ export async function createCheck( const updatedCreateCheckRequestJSON: CheckCreate = await rippleClient.autofill(createCheckRequestJSON); - console.log(`Signing Create Check for Vault ${vaultUUID}:`, updatedCreateCheckRequestJSON); - - const signCreateCheckResponse: SignResponse = rippleWallet.sign(updatedCreateCheckRequestJSON); - - const submitCreateCheckResponse: TxResponse = - await rippleClient.submitAndWait(signCreateCheckResponse.tx_blob); - - console.log( - `Response for submitted Create Check for Vault ${vaultUUID} request:`, - submitCreateCheckResponse - ); - - checkRippleTransactionResult(submitCreateCheckResponse); - - return submitCreateCheckResponse.result.hash; + return updatedCreateCheckRequestJSON; } catch (error) { throw new RippleError(`Error creating Check for Vault ${vaultUUID}: ${error}`); } From 6d7af06b19f6386699c45304a5dbe8fd4c626c88 Mon Sep 17 00:00:00 2001 From: sosaucily Date: Mon, 14 Oct 2024 15:16:33 -0400 Subject: [PATCH 09/27] remove NFT burning, only mint more --- src/network-handlers/ripple-handler.ts | 97 ++++---------------------- 1 file changed, 14 insertions(+), 83 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 0cd49f1..4d017f3 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -127,7 +127,9 @@ export class RippleHandler { if (matchingNFT.length === 0) { throw new RippleError(`Vault with UUID: ${nftUUID} not found`); } else if (matchingNFT.length > 1) { - throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`); + // we have multiple NFTs with the same UUID, this is not a problem + // let's just return the one with the highest nft_serial + matchingNFT.sort((a, b) => a.nft_serial - b.nft_serial); } const matchingVault: RawVault = decodeURI(matchingNFT[0].URI!); return lowercaseHexFields(matchingVault); @@ -150,7 +152,7 @@ export class RippleHandler { } } - async withdraw(uuid: string, withdrawAmount: bigint): Promise { + async withdraw(uuid: string, withdrawAmount: bigint): Promise { // Things like withdraw and deposit should get the existing NFT vault // then burn the NFT, and mint a new one with the updated value // putting the UUID into the URI @@ -162,18 +164,16 @@ export class RippleHandler { 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]; + const mintSig = await this.mintNFT(thisVault); + return mintSig; } catch (error) { throw new RippleError(`Unable to perform Withdraw for User: ${error}`); } } async setVaultStatusFunded( - burnNFTSignedTxBlobs: string[], mintTokensSignedTxBlobs: string[], // this can be a set of empty string is no tokens are being minted mintNFTSignedTxBlobs: string[] ): Promise { @@ -181,17 +181,6 @@ export class RippleHandler { await this.client.connect(); } 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 $$'); @@ -227,7 +216,6 @@ export class RippleHandler { async performCheckCashAndNftUpdate( cashCheckSignedTxBlobs: string[], - burnNFTSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { if (!this.client.isConnected()) { @@ -235,7 +223,7 @@ export class RippleHandler { } try { console.log('Doing the check cashing'); - // multisig burn + // multisig cash check const cash_check_tx = xrpl.multisign(cashCheckSignedTxBlobs); const cashCheckTx: xrpl.TxResponse = await this.client.submitAndWait(cash_check_tx); // add timeouts @@ -244,18 +232,6 @@ export class RippleHandler { 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 @@ -275,26 +251,11 @@ export class RippleHandler { } } - async setVaultStatusPending( - burnNFTSignedTxBlobs: string[], - mintNFTSignedTxBlobs: string[] - ): Promise { + async setVaultStatusPending(mintNFTSignedTxBlobs: string[]): Promise { if (!this.client.isConnected()) { await this.client.connect(); } 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 @@ -359,36 +320,7 @@ export class RippleHandler { } } - async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } - 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, 3); // this hardcoded number should match the number of active signers - - // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - preparedBurnTx.LastLedgerSequence = - Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? - - 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; - } - - async mintNFT(vault: RawVault, incrementBy: number = 0): Promise { + async mintNFT(vault: RawVault, incrementBy: number = 1): Promise { if (!this.client.isConnected()) { await this.client.connect(); } @@ -407,9 +339,8 @@ export class RippleHandler { // 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 preparedMintTx.LastLedgerSequence = Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; - if (incrementBy > 0) { - preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; - } + + preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; console.log('preparedMintTx ', preparedMintTx); @@ -503,11 +434,11 @@ export class RippleHandler { `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` ); } - const two_more_sigs = await this.withdraw( + const mint_sig = await this.withdraw( vault.uuid, BigInt(shiftValue(Number(checkSendMax.value))) ); - return [my_check_cash_sig, ...two_more_sigs]; + return [my_check_cash_sig, mint_sig]; } catch (error) { console.error(`Error cashing Check: ${error} \n continuing`); } @@ -520,7 +451,7 @@ export class RippleHandler { await this.client.connect(); } - //what's + // remove this?? if ( [ '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA', From 981056066ed3f11a247f8b40fe3939977dcd80b9 Mon Sep 17 00:00:00 2001 From: sosaucily Date: Mon, 14 Oct 2024 15:37:48 -0400 Subject: [PATCH 10/27] sort by nft_serial when getting all vaults --- src/network-handlers/ripple-handler.ts | 41 +++++++++----------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 4d017f3..707e1e2 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -122,8 +122,11 @@ export class RippleHandler { let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - const nftTokenId = await this.getNFTokenIdForVault(nftUUID); - const matchingNFT = nfts.result.account_nfts.filter(nft => nft.NFTokenID === nftTokenId); + + const matchingNFT = nfts.result.account_nfts.filter( + nft => decodeURI(nft.URI!).uuid.slice(2) === uuid + ); + if (matchingNFT.length === 0) { throw new RippleError(`Vault with UUID: ${nftUUID} not found`); } else if (matchingNFT.length > 1) { @@ -285,38 +288,22 @@ export class RippleHandler { account: this.issuerAddress, }; + // does this return all the NFTs? or only a max of 50 or so, and need pagination? + // https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/nftokenpage const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); const allNFTs = nfts.result.account_nfts; const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeURI(nft.URI!))); - return allVaults; - } catch (error) { - throw new RippleError(`Could not fetch All Vaults: ${error}`); - } - } - - async getNFTokenIdForVault(uuid: string): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } - console.log(`Getting NFTokenId for vault: ${uuid}`); - try { - const getNFTsTransaction: AccountNFTsRequest = { - command: 'account_nfts', - account: this.issuerAddress, - }; + const uniqueUUIDs = Array.from(new Set(allVaults.map(vault => vault.uuid))); + const uniqueVaults: RawVault[] = []; - const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - const matchingNFT = nfts.result.account_nfts.find( - nft => decodeURI(nft.URI!).uuid.slice(2) === uuid - ); + uniqueUUIDs.forEach(async uuid => { + uniqueVaults.push(await this.getRawVault(uuid)); + }); - if (!matchingNFT) { - throw new RippleError(`Vault for uuid: ${uuid} not found`); - } - return matchingNFT.NFTokenID; + return uniqueVaults; } catch (error) { - throw new RippleError(`Could not find NFTokenId for vault Vault: ${error}`); + throw new RippleError(`Could not fetch All Vaults: ${error}`); } } From 112d065552c69c21feceda656fd1ca32771da9a8 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 15 Oct 2024 10:29:55 +0200 Subject: [PATCH 11/27] feat: add ledger-xrp-handler --- package.json | 2 + src/functions/ripple/ripple.functions.ts | 14 +- src/index.ts | 2 + src/network-handlers/xrp-ledger-handler.ts | 146 +++++++++++++++++++++ yarn.lock | 12 +- 5 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 src/network-handlers/xrp-ledger-handler.ts diff --git a/package.json b/package.json index 0b4bbd5..8317645 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@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", @@ -72,6 +73,7 @@ "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/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index f0017f3..a35dda6 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -274,17 +274,13 @@ export async function signAndSubmitRippleTransaction( export async function getLockedBTCBalance( rippleClient: Client, - rippleWallet: Wallet, + userAddress: string, issuerAddress: string ): Promise { try { await connectRippleClient(rippleClient); - const rippleVaults = await getAllRippleVaults( - rippleClient, - issuerAddress, - rippleWallet.classicAddress - ); + const rippleVaults = await getAllRippleVaults(rippleClient, issuerAddress, userAddress); const lockedBTCBalance = rippleVaults.reduce((accumulator, vault) => { return accumulator + vault.valueLocked.toNumber(); @@ -298,7 +294,7 @@ export async function getLockedBTCBalance( export async function getDLCBTCBalance( rippleClient: Client, - rippleWallet: Wallet, + userAddress: string, issuerAddress: string ): Promise { try { @@ -306,7 +302,7 @@ export async function getDLCBTCBalance( const accountNonXRPBalancesRequest: AccountLinesRequest = { command: 'account_lines', - account: rippleWallet.classicAddress, + account: userAddress, ledger_index: 'validated', }; @@ -334,7 +330,7 @@ export async function createCheck( destinationTag: number = 1, dlcBTCAmount: string, vaultUUID: string -): Promise { +): Promise { try { await connectRippleClient(rippleClient); diff --git a/src/index.ts b/src/index.ts index 1a25c2f..c7ccb90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,14 @@ 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 { 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, EthereumHandler, ProofOfReserveHandler, RippleHandler, diff --git a/src/network-handlers/xrp-ledger-handler.ts b/src/network-handlers/xrp-ledger-handler.ts new file mode 100644 index 0000000..e896b1f --- /dev/null +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -0,0 +1,146 @@ +import Xrp from '@ledgerhq/hw-app-xrp'; +import { encode } from 'ripple-binary-codec'; +import { CheckCreate, Client, Transaction, TrustSet } from 'xrpl'; + +import { + checkRippleTransactionResult, + connectRippleClient, + createCheck, + getDLCBTCBalance, + getLockedBTCBalance, + setTrustLine, +} from '../functions/ripple/ripple.functions.js'; + +export class LedgerXRPHandler { + private ledgerApp: Xrp.default; + private derivationPath: string; + private xrpClient: Client; + private issuerAddress: string; + + constructor( + ledgerApp: Xrp.default, + derivationPath: string, + xrpClient: Client, + issuerAddress: string + ) { + this.ledgerApp = ledgerApp; + this.derivationPath = derivationPath; + this.xrpClient = xrpClient; + this.issuerAddress = issuerAddress; + } + + public async getAddress(): Promise { + const address = await this.ledgerApp.getAddress(this.derivationPath); + return address.address; + } + + public async setTrustLine(): Promise { + try { + const deviceData = await this.ledgerApp.getAddress(this.derivationPath); + + const trustLineRequest = await setTrustLine( + this.xrpClient, + deviceData.address, + this.issuerAddress + ); + + if (!trustLineRequest) { + console.error('TrustLine is already set'); + return; + } + const updatedTrustLineRequest: TrustSet = { + ...trustLineRequest, + Flags: 2147483648, + SigningPubKey: deviceData.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 deviceData = await this.ledgerApp.getAddress(this.derivationPath); + + const checkCreateRequest: CheckCreate = await createCheck( + this.xrpClient, + deviceData.address, + this.issuerAddress, + undefined, + dlcBTCAmount, + vaultUUID + ); + + const updatedCheckCreateRequest: CheckCreate = { + ...checkCreateRequest, + Flags: 2147483648, + SigningPubKey: deviceData.publicKey.toUpperCase(), + }; + + const encodedCheckCreateRequest = encode(updatedCheckCreateRequest); + + const signature = await this.ledgerApp.signTransaction( + this.derivationPath, + encodedCheckCreateRequest + ); + + const signedCheckCreateRequest: Transaction = { + ...updatedCheckCreateRequest, + TxnSignature: signature, + }; + + await connectRippleClient(this.xrpClient); + + const submitCheckCreateRequestResponse = + await this.xrpClient.submitAndWait(signedCheckCreateRequest); + + console.log(`Response for submitted Transaction Request:`, submitCheckCreateRequestResponse); + + checkRippleTransactionResult(submitCheckCreateRequestResponse); + } catch (error) { + throw new Error(`Error creating Check: ${error}`); + } + } + + public async getDLCBTCBalance(): Promise { + try { + const deviceData = await this.ledgerApp.getAddress(this.derivationPath); + + return await getDLCBTCBalance(this.xrpClient, deviceData.address, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } + + public async getLockedBTCBalance(): Promise { + try { + const deviceData = await this.ledgerApp.getAddress(this.derivationPath); + + return await getLockedBTCBalance(this.xrpClient, deviceData.address, this.issuerAddress); + } catch (error) { + throw new Error(`Error getting BTC Balance: ${error}`); + } + } +} diff --git a/yarn.lock b/yarn.lock index 9a7a12a..f9fa8a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1105,6 +1105,14 @@ tiny-secp256k1 "1.1.6" varuint-bitcoin "1.1.2" +"@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" @@ -1115,7 +1123,7 @@ "@ledgerhq/logs" "^6.12.0" events "^3.3.0" -"@ledgerhq/hw-transport@^6.31.2": +"@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== @@ -1792,7 +1800,7 @@ 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== From a7ff441fc169ff11052ec3a12c910db1b24a6841 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 15 Oct 2024 11:38:31 +0200 Subject: [PATCH 12/27] chore: revert "sort by nft_serial when getting all vaults" This reverts commit 981056066ed3f11a247f8b40fe3939977dcd80b9. --- src/network-handlers/ripple-handler.ts | 41 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 707e1e2..4d017f3 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -122,11 +122,8 @@ export class RippleHandler { let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - - const matchingNFT = nfts.result.account_nfts.filter( - nft => decodeURI(nft.URI!).uuid.slice(2) === uuid - ); - + const nftTokenId = await this.getNFTokenIdForVault(nftUUID); + const matchingNFT = nfts.result.account_nfts.filter(nft => nft.NFTokenID === nftTokenId); if (matchingNFT.length === 0) { throw new RippleError(`Vault with UUID: ${nftUUID} not found`); } else if (matchingNFT.length > 1) { @@ -288,22 +285,38 @@ export class RippleHandler { account: this.issuerAddress, }; - // does this return all the NFTs? or only a max of 50 or so, and need pagination? - // https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/nftokenpage const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); const allNFTs = nfts.result.account_nfts; const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeURI(nft.URI!))); - const uniqueUUIDs = Array.from(new Set(allVaults.map(vault => vault.uuid))); - const uniqueVaults: RawVault[] = []; + return allVaults; + } catch (error) { + throw new RippleError(`Could not fetch All Vaults: ${error}`); + } + } + + async getNFTokenIdForVault(uuid: string): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + console.log(`Getting NFTokenId for vault: ${uuid}`); + try { + const getNFTsTransaction: AccountNFTsRequest = { + command: 'account_nfts', + account: this.issuerAddress, + }; - uniqueUUIDs.forEach(async uuid => { - uniqueVaults.push(await this.getRawVault(uuid)); - }); + const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); + const matchingNFT = nfts.result.account_nfts.find( + nft => decodeURI(nft.URI!).uuid.slice(2) === uuid + ); - return uniqueVaults; + if (!matchingNFT) { + throw new RippleError(`Vault for uuid: ${uuid} not found`); + } + return matchingNFT.NFTokenID; } catch (error) { - throw new RippleError(`Could not fetch All Vaults: ${error}`); + throw new RippleError(`Could not find NFTokenId for vault Vault: ${error}`); } } From cfbd63735cdb3595a24f9138c61dfaf3a2ad17cc Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 15 Oct 2024 11:40:52 +0200 Subject: [PATCH 13/27] chore: revert "remove NFT burning, only mint more" This reverts commit 6d7af06b19f6386699c45304a5dbe8fd4c626c88. --- src/network-handlers/ripple-handler.ts | 97 ++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 4d017f3..0cd49f1 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -127,9 +127,7 @@ export class RippleHandler { if (matchingNFT.length === 0) { throw new RippleError(`Vault with UUID: ${nftUUID} not found`); } else if (matchingNFT.length > 1) { - // we have multiple NFTs with the same UUID, this is not a problem - // let's just return the one with the highest nft_serial - matchingNFT.sort((a, b) => a.nft_serial - b.nft_serial); + throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`); } const matchingVault: RawVault = decodeURI(matchingNFT[0].URI!); return lowercaseHexFields(matchingVault); @@ -152,7 +150,7 @@ export class RippleHandler { } } - async withdraw(uuid: string, withdrawAmount: bigint): Promise { + async withdraw(uuid: string, withdrawAmount: bigint): Promise { // Things like withdraw and deposit should get the existing NFT vault // then burn the NFT, and mint a new one with the updated value // putting the UUID into the URI @@ -164,16 +162,18 @@ export class RippleHandler { 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); - return mintSig; + 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[], // this can be a set of empty string is no tokens are being minted mintNFTSignedTxBlobs: string[] ): Promise { @@ -181,6 +181,17 @@ export class RippleHandler { await this.client.connect(); } 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 $$'); @@ -216,6 +227,7 @@ export class RippleHandler { async performCheckCashAndNftUpdate( cashCheckSignedTxBlobs: string[], + burnNFTSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { if (!this.client.isConnected()) { @@ -223,7 +235,7 @@ export class RippleHandler { } try { console.log('Doing the check cashing'); - // multisig cash check + // multisig burn const cash_check_tx = xrpl.multisign(cashCheckSignedTxBlobs); const cashCheckTx: xrpl.TxResponse = await this.client.submitAndWait(cash_check_tx); // add timeouts @@ -232,6 +244,18 @@ export class RippleHandler { 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 @@ -251,11 +275,26 @@ export class RippleHandler { } } - async setVaultStatusPending(mintNFTSignedTxBlobs: string[]): Promise { + async setVaultStatusPending( + burnNFTSignedTxBlobs: string[], + mintNFTSignedTxBlobs: string[] + ): Promise { if (!this.client.isConnected()) { await this.client.connect(); } 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 @@ -320,7 +359,36 @@ export class RippleHandler { } } - async mintNFT(vault: RawVault, incrementBy: number = 1): Promise { + async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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, 3); // this hardcoded number should match the number of active signers + + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + preparedBurnTx.LastLedgerSequence = + Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? + + 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; + } + + async mintNFT(vault: RawVault, incrementBy: number = 0): Promise { if (!this.client.isConnected()) { await this.client.connect(); } @@ -339,8 +407,9 @@ export class RippleHandler { // 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 preparedMintTx.LastLedgerSequence = Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; - - preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; + if (incrementBy > 0) { + preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; + } console.log('preparedMintTx ', preparedMintTx); @@ -434,11 +503,11 @@ export class RippleHandler { `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` ); } - const mint_sig = await this.withdraw( + const two_more_sigs = await this.withdraw( vault.uuid, BigInt(shiftValue(Number(checkSendMax.value))) ); - return [my_check_cash_sig, mint_sig]; + return [my_check_cash_sig, ...two_more_sigs]; } catch (error) { console.error(`Error cashing Check: ${error} \n continuing`); } @@ -451,7 +520,7 @@ export class RippleHandler { await this.client.connect(); } - // remove this?? + //what's if ( [ '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA', From f73dc87f4c0a29783e586229d56433a8ff0ef7f1 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 15 Oct 2024 18:46:12 +0200 Subject: [PATCH 14/27] feat: add gem wallet xrp handler --- package.json | 1 + .../software-wallet-dlc-handler.ts | 33 +---- src/index.ts | 2 + .../xrp-gem-wallet-handler.ts | 136 ++++++++++++++++++ src/network-handlers/xrp-ledger-handler.ts | 30 ++-- yarn.lock | 5 + 6 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 src/network-handlers/xrp-gem-wallet-handler.ts diff --git a/package.json b/package.json index 8317645..ab6a7ab 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "typescript-eslint": "^7.7.0" }, "dependencies": { + "@gemwallet/api": "^3.8.0", "@ledgerhq/hw-app-btc": "10.4.1", "@ledgerhq/hw-app-xrp": "6.29.4", "@noble/hashes": "1.4.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/index.ts b/src/index.ts index c7ccb90..4a268d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ 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'; @@ -11,6 +12,7 @@ export { LedgerDLCHandler, SoftwareWalletDLCHandler, LedgerXRPHandler, + GemXRPHandler, EthereumHandler, ProofOfReserveHandler, RippleHandler, 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..2d255e9 --- /dev/null +++ b/src/network-handlers/xrp-gem-wallet-handler.ts @@ -0,0 +1,136 @@ +import { getAddress, signTransaction } from '@gemwallet/api'; +import { ResponseType } from '@gemwallet/api/_constants/index.js'; +import { CheckCreate, Client, TrustSet } from 'xrpl'; + +import { + checkRippleTransactionResult, + connectRippleClient, + createCheck, + getDLCBTCBalance, + getLockedBTCBalance, + setTrustLine, +} from '../functions/ripple/ripple.functions.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, + }; + + const signCheckCreateResponse = await signTransaction({ + transaction: updatedCheckCreateRequest, + }); + + 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); + } catch (error) { + throw new Error(`Error creating Check: ${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 index e896b1f..8c588cf 100644 --- a/src/network-handlers/xrp-ledger-handler.ts +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -16,17 +16,23 @@ export class LedgerXRPHandler { 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 + 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 { @@ -36,11 +42,9 @@ export class LedgerXRPHandler { public async setTrustLine(): Promise { try { - const deviceData = await this.ledgerApp.getAddress(this.derivationPath); - const trustLineRequest = await setTrustLine( this.xrpClient, - deviceData.address, + this.userAddress, this.issuerAddress ); @@ -51,7 +55,7 @@ export class LedgerXRPHandler { const updatedTrustLineRequest: TrustSet = { ...trustLineRequest, Flags: 2147483648, - SigningPubKey: deviceData.publicKey.toUpperCase(), + SigningPubKey: this.publicKey.toUpperCase(), }; const encodedTrustLineRequest = encode(updatedTrustLineRequest); @@ -82,11 +86,9 @@ export class LedgerXRPHandler { public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { try { - const deviceData = await this.ledgerApp.getAddress(this.derivationPath); - const checkCreateRequest: CheckCreate = await createCheck( this.xrpClient, - deviceData.address, + this.userAddress, this.issuerAddress, undefined, dlcBTCAmount, @@ -96,7 +98,7 @@ export class LedgerXRPHandler { const updatedCheckCreateRequest: CheckCreate = { ...checkCreateRequest, Flags: 2147483648, - SigningPubKey: deviceData.publicKey.toUpperCase(), + SigningPubKey: this.publicKey.toUpperCase(), }; const encodedCheckCreateRequest = encode(updatedCheckCreateRequest); @@ -126,9 +128,8 @@ export class LedgerXRPHandler { public async getDLCBTCBalance(): Promise { try { - const deviceData = await this.ledgerApp.getAddress(this.derivationPath); - - return await getDLCBTCBalance(this.xrpClient, deviceData.address, this.issuerAddress); + await connectRippleClient(this.xrpClient); + return await getDLCBTCBalance(this.xrpClient, this.userAddress, this.issuerAddress); } catch (error) { throw new Error(`Error getting BTC Balance: ${error}`); } @@ -136,9 +137,8 @@ export class LedgerXRPHandler { public async getLockedBTCBalance(): Promise { try { - const deviceData = await this.ledgerApp.getAddress(this.derivationPath); - - return await getLockedBTCBalance(this.xrpClient, deviceData.address, this.issuerAddress); + 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/yarn.lock b/yarn.lock index f9fa8a2..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" From ec5a5c9b218dbb83525ee104b39ec617a3bec37c Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 15 Oct 2024 19:04:15 +0200 Subject: [PATCH 15/27] feat: modify ripplehandler to have websockerurl argument in constructor --- src/network-handlers/ripple-handler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 0cd49f1..6cc2aab 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -58,14 +58,14 @@ export class RippleHandler { private wallet: xrpl.Wallet; private issuerAddress: string; - private constructor(seedPhrase: string, issuerAddress: string) { - this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233'); + private constructor(seedPhrase: string, issuerAddress: string, websocketURL: string) { + this.client = new xrpl.Client(websocketURL); this.wallet = xrpl.Wallet.fromSeed(seedPhrase); this.issuerAddress = issuerAddress; } - static fromSeed(seedPhrase: string, issuerAddress: string): RippleHandler { - return new RippleHandler(seedPhrase, issuerAddress); + static fromSeed(seedPhrase: string, issuerAddress: string, websocketURL: string): RippleHandler { + return new RippleHandler(seedPhrase, issuerAddress, websocketURL); } async submit(signatures: string[]): Promise { From 0ba53c94a11c18aa4275c83a6f468f6f16a5f9bb Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 16 Oct 2024 14:43:11 +0200 Subject: [PATCH 16/27] feat: add minSigners argument to ripplehandler --- src/network-handlers/ripple-handler.ts | 28 +++++++++++++++------- src/network-handlers/xrp-ledger-handler.ts | 2 +- tests/unit/request-functions.test.ts | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 6cc2aab..3d0e048 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -57,15 +57,27 @@ export class RippleHandler { private client: xrpl.Client; private wallet: xrpl.Wallet; private issuerAddress: string; - - private constructor(seedPhrase: string, issuerAddress: string, websocketURL: string) { + private minSigners: number; + + private constructor( + seedPhrase: string, + issuerAddress: string, + websocketURL: string, + minSigners: number + ) { this.client = new xrpl.Client(websocketURL); this.wallet = xrpl.Wallet.fromSeed(seedPhrase); this.issuerAddress = issuerAddress; + this.minSigners = minSigners; } - static fromSeed(seedPhrase: string, issuerAddress: string, websocketURL: string): RippleHandler { - return new RippleHandler(seedPhrase, issuerAddress, websocketURL); + static fromSeed( + seedPhrase: string, + issuerAddress: string, + websocketURL: string, + minSigners: number + ): RippleHandler { + return new RippleHandler(seedPhrase, issuerAddress, websocketURL, minSigners); } async submit(signatures: string[]): Promise { @@ -370,7 +382,7 @@ export class RippleHandler { Account: this.issuerAddress, NFTokenID: nftTokenId, }; - const preparedBurnTx = await this.client.autofill(burnTransactionJson, 3); // this hardcoded number should match the number of active signers + const preparedBurnTx = await this.client.autofill(burnTransactionJson, this.minSigners); // this hardcoded number should match the number of active signers // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 // 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 @@ -401,7 +413,7 @@ export class RippleHandler { URI: newURI, NFTokenTaxon: 0, }; - const preparedMintTx = await this.client.autofill(mintTransactionJson, 3); + const preparedMintTx = await this.client.autofill(mintTransactionJson, this.minSigners); // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 // 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 @@ -545,7 +557,7 @@ export class RippleHandler { const updatedCashCheckTransactionJSON: CheckCash = await this.client.autofill( cashCheckTransactionJSON, - 3 // hardcoded? not good? just fee related? + this.minSigners ); // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 @@ -598,7 +610,7 @@ export class RippleHandler { const updatedSendTokenTransactionJSON: Payment = await this.client.autofill( sendTokenTransactionJSON, - 3 + this.minSigners ); // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 diff --git a/src/network-handlers/xrp-ledger-handler.ts b/src/network-handlers/xrp-ledger-handler.ts index 8c588cf..59a7017 100644 --- a/src/network-handlers/xrp-ledger-handler.ts +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -1,4 +1,4 @@ -import Xrp from '@ledgerhq/hw-app-xrp'; +import * as Xrp from '@ledgerhq/hw-app-xrp'; import { encode } from 'ripple-binary-codec'; import { CheckCreate, Client, Transaction, TrustSet } from 'xrpl'; 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 - `) ); }); From b75773e21575681d9d174d2707074f424113e598 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 16 Oct 2024 14:51:59 +0200 Subject: [PATCH 17/27] chore: bump version --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ab6a7ab..709ec49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.3.0", + "version": "2.4.0", "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", @@ -59,7 +59,7 @@ "typescript-eslint": "^7.7.0" }, "dependencies": { - "@gemwallet/api": "^3.8.0", + "@gemwallet/api": "3.8.0", "@ledgerhq/hw-app-btc": "10.4.1", "@ledgerhq/hw-app-xrp": "6.29.4", "@noble/hashes": "1.4.0", @@ -74,7 +74,7 @@ "ledger-bitcoin": "0.2.3", "prompts": "2.4.2", "ramda": "0.30.1", - "ripple-binary-codec": "^2.1.0", + "ripple-binary-codec": "2.1.0", "scure": "1.6.0", "tiny-secp256k1": "2.2.3", "xrpl": "4.0.0" From 5f0b1bad78b668705f1bc05e5294b43b1b1af70c Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 17 Oct 2024 13:29:05 +0200 Subject: [PATCH 18/27] feat: add timestamp to xrpl vaults --- src/network-handlers/ripple-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 3d0e048..30cb6e2 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -148,7 +148,7 @@ export class RippleHandler { } } - async setupVault(uuid: string, userAddress: string): Promise { + async setupVault(uuid: string, userAddress: string, timeStamp: number): Promise { if (!this.client.isConnected()) { await this.client.connect(); } @@ -156,6 +156,7 @@ export class RippleHandler { const newVault = buildDefaultNftVault(); newVault.uuid = uuid; newVault.creator = userAddress; + newVault.timestamp = BigNumber.from(timeStamp); return await this.mintNFT(newVault); } catch (error) { throw new RippleError(`Could not setup Ripple Vault: ${error}`); From a6431203aa2fbe7cf0bb3f8cf0a63501623f0be3 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 17 Oct 2024 14:23:58 +0200 Subject: [PATCH 19/27] feat: modify currency symbol for xrpl --- package.json | 2 +- src/functions/ripple/ripple.functions.ts | 8 ++++---- src/network-handlers/ripple-handler.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 709ec49..17c3a14 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.0", + "version": "2.4.1", "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", diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index a35dda6..763da04 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -165,7 +165,7 @@ export async function setTrustLine( result: { lines }, }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); - if (lines.some(line => line.currency === 'DLC' && line.account === issuerAddress)) { + if (lines.some(line => line.currency === 'BTC' && line.account === issuerAddress)) { console.log(`Trust Line already exists for Issuer: ${issuerAddress}`); return; } @@ -174,7 +174,7 @@ export async function setTrustLine( TransactionType: 'TrustSet', Account: ownerAddress, LimitAmount: { - currency: 'DLC', + currency: 'BTC', issuer: issuerAddress, value: '10000000000', }, @@ -311,7 +311,7 @@ export async function getDLCBTCBalance( }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); const dlcBTCBalance = lines.find( - line => line.currency === 'DLC' && line.account === issuerAddress + line => line.currency === 'BTC' && line.account === issuerAddress ); if (!dlcBTCBalance) { return 0; @@ -345,7 +345,7 @@ export async function createCheck( Destination: destinationAddress, DestinationTag: destinationTag, SendMax: { - currency: 'DLC', + currency: 'BTC', value: shiftedAmountAsNumber.toString(), issuer: destinationAddress, }, diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 3d0e048..5969b08 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -536,6 +536,10 @@ export class RippleHandler { if ( [ '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA', + 'AD2F46F345B07A070CBB0797D47D4D1A6CAA2BB541A0932014B909194665E4D5', + 'DDF48640BF7DBC84FD08C1129999415CF5745095871FA6E50F1EAEB8AB032654', + 'BA8B88BC1FD746F538AA6EFD1F1BFC982E3CCDD93EDDCFB4541EA608B355D778', + '48A7965799596CDA23C774778CD2D65E3E7C01648077539840A51ABD54791E32', '93BAA031806AE4902933C1EE9B66E7EBAF0F7A182314085BEFF99DF080A1CBCB', 'F51C7E3CCFD2EC8CA9A460A34C5BC185E9466031865E76736C0A60BC3F7C7316', ].includes(checkID) @@ -549,7 +553,7 @@ export class RippleHandler { Account: this.issuerAddress, CheckID: checkID, Amount: { - currency: 'DLC', + currency: 'BTC', value: dlcBTCAmount, issuer: this.issuerAddress, }, @@ -602,7 +606,7 @@ export class RippleHandler { Destination: destinationAddress, DestinationTag: 1, Amount: { - currency: 'DLC', + currency: 'BTC', value: dlcBTCAmount, issuer: this.issuerAddress, }, From cb9348c2eb3a2d1a35768aa75d58a09d80b053c3 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 17 Oct 2024 19:50:30 +0200 Subject: [PATCH 20/27] feat: modify limit of get nfts --- package.json | 2 +- src/functions/ripple/ripple.functions.ts | 1 + src/network-handlers/ripple-handler.ts | 103 ++++++++++++----------- 3 files changed, 58 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 709ec49..2538b60 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.0", + "version": "2.4.2", "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", diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index a35dda6..6ce3375 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -232,6 +232,7 @@ export async function getAllRippleVaults( const getAccountNFTsRequest: AccountNFTsRequest = { command: 'account_nfts', account: issuerAddress, + limit: 400, }; const { diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 3d0e048..c4d07f8 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -334,6 +334,7 @@ export class RippleHandler { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', account: this.issuerAddress, + limit: 400, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); @@ -372,62 +373,70 @@ export class RippleHandler { } async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } - 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); // this hardcoded number should match the number of active signers + try { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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); // this hardcoded number should match the number of active signers - // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - preparedBurnTx.LastLedgerSequence = - Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + preparedBurnTx.LastLedgerSequence = + Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? - if (incrementBy > 0) { - preparedBurnTx.Sequence = preparedBurnTx.Sequence! + incrementBy; - } + if (incrementBy > 0) { + preparedBurnTx.Sequence = preparedBurnTx.Sequence! + incrementBy; + } - console.log('preparedBurnTx ', preparedBurnTx); + console.log('preparedBurnTx ', preparedBurnTx); - const sig = this.wallet.sign(preparedBurnTx, true); - // console.log('tx_one_sig: ', sig); - return sig.tx_blob; + 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 { - if (!this.client.isConnected()) { - await this.client.connect(); - } - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - preparedMintTx.LastLedgerSequence = - Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; - if (incrementBy > 0) { - preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; - } + try { + if (!this.client.isConnected()) { + await this.client.connect(); + } + 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + preparedMintTx.LastLedgerSequence = + Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; + if (incrementBy > 0) { + preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; + } - console.log('preparedMintTx ', preparedMintTx); + console.log('preparedMintTx ', preparedMintTx); - const sig = this.wallet.sign(preparedMintTx, true); - console.log('tx_one_sig: ', sig); - return sig.tx_blob; + 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 { From 3c40342712e5870387deb648a620130ed8d50dee Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 17 Oct 2024 22:31:22 +0200 Subject: [PATCH 21/27] chore: bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2538b60..c128c2c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.2", + "version": "2.4.3", "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", From 97300763b2be66ea604f24bdf02a208d7bf16ccb Mon Sep 17 00:00:00 2001 From: Polybius93 <99192647+Polybius93@users.noreply.github.com> Date: Fri, 18 Oct 2024 18:39:43 +0200 Subject: [PATCH 22/27] feat: add cash check by tx id function (#33) * feat: add cash check by tx id function * chore: bump package version * feat: modify currency to dlcbtc, add functions to xrphandlers --- package.json | 2 +- src/constants/ripple.constants.ts | 3 + .../attestor/attestor-request.functions.ts | 5 ++ src/functions/attestor/index.ts | 1 + src/functions/ripple/ripple.functions.ts | 48 +++++++++-- src/network-handlers/ripple-handler.ts | 79 ++++++++----------- .../xrp-gem-wallet-handler.ts | 24 +++++- src/network-handlers/xrp-ledger-handler.ts | 27 ++++++- 8 files changed, 131 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index c128c2c..3f4c08c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.3", + "version": "2.4.5", "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", diff --git a/src/constants/ripple.constants.ts b/src/constants/ripple.constants.ts index e676c76..a9fe18d 100644 --- a/src/constants/ripple.constants.ts +++ b/src/constants/ripple.constants.ts @@ -1 +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/functions/attestor/attestor-request.functions.ts b/src/functions/attestor/attestor-request.functions.ts index b3b731f..e3c07c3 100644 --- a/src/functions/attestor/attestor-request.functions.ts +++ b/src/functions/attestor/attestor-request.functions.ts @@ -15,6 +15,11 @@ export async function submitSetupXRPLVaultRequest( return sendRequest(`${coordinatorURL}/app/setup-xrpl-vault`, requestBody); } +export async function submitXRPLCheckToCash(coordinatorURL: string, txHash: string): Promise { + const requestBody = JSON.stringify({ tx_hash: txHash }); + return sendRequest(`${coordinatorURL}/app/cash-xrpl-check`, requestBody); +} + export async function getAttestorExtendedGroupPublicKey(coordinatorURL: string): Promise { return sendGetRequest(`${coordinatorURL}/tss/get-extended-group-publickey`); } diff --git a/src/functions/attestor/index.ts b/src/functions/attestor/index.ts index d3b3240..88d8d90 100644 --- a/src/functions/attestor/index.ts +++ b/src/functions/attestor/index.ts @@ -3,4 +3,5 @@ export { submitWithdrawDepositPSBT, getAttestorExtendedGroupPublicKey, submitSetupXRPLVaultRequest, + submitXRPLCheckToCash, } from '../attestor/attestor-request.functions.js'; diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index 1772287..ae545b6 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -5,8 +5,10 @@ import { AccountLinesResponse, AccountNFToken, AccountNFTsRequest, + AccountObjectsRequest, CheckCreate, Client, + LedgerEntry, SubmittableTransaction, Transaction, TransactionMetadataBase, @@ -17,7 +19,10 @@ import { convertStringToHex, } from 'xrpl'; -import { TRANSACTION_SUCCESS_CODE } from '../../constants/ripple.constants.js'; +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'; @@ -165,7 +170,9 @@ export async function setTrustLine( result: { lines }, }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); - if (lines.some(line => line.currency === 'BTC' && line.account === issuerAddress)) { + if ( + lines.some(line => line.currency === XRPL_DLCBTC_CURRENCY_HEX && line.account === issuerAddress) + ) { console.log(`Trust Line already exists for Issuer: ${issuerAddress}`); return; } @@ -174,7 +181,7 @@ export async function setTrustLine( TransactionType: 'TrustSet', Account: ownerAddress, LimitAmount: { - currency: 'BTC', + currency: XRPL_DLCBTC_CURRENCY_HEX, issuer: issuerAddress, value: '10000000000', }, @@ -312,7 +319,7 @@ export async function getDLCBTCBalance( }: AccountLinesResponse = await rippleClient.request(accountNonXRPBalancesRequest); const dlcBTCBalance = lines.find( - line => line.currency === 'BTC' && line.account === issuerAddress + line => line.currency === XRPL_DLCBTC_CURRENCY_HEX && line.account === issuerAddress ); if (!dlcBTCBalance) { return 0; @@ -346,7 +353,7 @@ export async function createCheck( Destination: destinationAddress, DestinationTag: destinationTag, SendMax: { - currency: 'BTC', + currency: XRPL_DLCBTC_CURRENCY_HEX, value: shiftedAmountAsNumber.toString(), issuer: destinationAddress, }, @@ -361,3 +368,34 @@ export async function createCheck( 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/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 4d03fdd..4d2e252 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -6,14 +6,19 @@ import xrpl, { AccountObjectsResponse, CheckCash, IssuedCurrencyAmount, - LedgerEntry, Payment, Request, SubmittableTransaction, } from 'xrpl'; import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; -import { decodeURI, encodeURI } from '../functions/ripple/ripple.functions.js'; +import { XRPL_DLCBTC_CURRENCY_HEX } from '../constants/ripple.constants.js'; +import { + connectRippleClient, + decodeURI, + encodeURI, + getCheckByTXHash, +} 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'; @@ -130,6 +135,7 @@ export class RippleHandler { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', account: this.issuerAddress, + limit: 400, }; let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); @@ -357,6 +363,7 @@ export class RippleHandler { const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', account: this.issuerAddress, + limit: 400, }; const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); @@ -506,55 +513,37 @@ export class RippleHandler { return getAccountObjectsResponse.result.account_objects; } - async getAndCashAllChecksAndUpdateNFT(): Promise { - const allChecks = (await this.getAllChecks()) as LedgerEntry.Check[]; - // console.log('All Checks:', allChecks); - const allVaults = await this.getContractVaults(); + async getCashCheckAndWithdrawSignatures(txHash: string): Promise { + try { + const check = await getCheckByTXHash(this.client, this.issuerAddress, txHash); + const invoiceID = check.InvoiceID; - for (const check of allChecks) { - try { - const checkSendMax = check.SendMax as IssuedCurrencyAmount; + if (!invoiceID) { + throw new RippleError(`Could not find Invoice ID for Check with TX Hash: ${txHash}`); + } - const my_check_cash_sig = await this.cashCheck(check.index, checkSendMax.value); + const vault = await this.getRawVault(`0x${invoiceID}`.toLowerCase()); - const vault = allVaults.find( - vault => vault.uuid.toUpperCase().slice(2) === check.InvoiceID - ); - if (!vault) { - throw new RippleError( - `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` - ); - } - const two_more_sigs = await this.withdraw( - vault.uuid, - BigInt(shiftValue(Number(checkSendMax.value))) - ); - return [my_check_cash_sig, ...two_more_sigs]; - } catch (error) { - console.error(`Error cashing Check: ${error} \n continuing`); + 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}`); } - return []; } async cashCheck(checkID: string, dlcBTCAmount: string): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } - - //what's - if ( - [ - '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA', - 'AD2F46F345B07A070CBB0797D47D4D1A6CAA2BB541A0932014B909194665E4D5', - 'DDF48640BF7DBC84FD08C1129999415CF5745095871FA6E50F1EAEB8AB032654', - 'BA8B88BC1FD746F538AA6EFD1F1BFC982E3CCDD93EDDCFB4541EA608B355D778', - '48A7965799596CDA23C774778CD2D65E3E7C01648077539840A51ABD54791E32', - '93BAA031806AE4902933C1EE9B66E7EBAF0F7A182314085BEFF99DF080A1CBCB', - 'F51C7E3CCFD2EC8CA9A460A34C5BC185E9466031865E76736C0A60BC3F7C7316', - ].includes(checkID) - ) - throw new Error('Invalid Check'); + await connectRippleClient(this.client); console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`); @@ -563,7 +552,7 @@ export class RippleHandler { Account: this.issuerAddress, CheckID: checkID, Amount: { - currency: 'BTC', + currency: XRPL_DLCBTC_CURRENCY_HEX, value: dlcBTCAmount, issuer: this.issuerAddress, }, @@ -616,7 +605,7 @@ export class RippleHandler { Destination: destinationAddress, DestinationTag: 1, Amount: { - currency: 'BTC', + currency: XRPL_DLCBTC_CURRENCY_HEX, value: dlcBTCAmount, issuer: this.issuerAddress, }, diff --git a/src/network-handlers/xrp-gem-wallet-handler.ts b/src/network-handlers/xrp-gem-wallet-handler.ts index 2d255e9..a7337c0 100644 --- a/src/network-handlers/xrp-gem-wallet-handler.ts +++ b/src/network-handlers/xrp-gem-wallet-handler.ts @@ -2,6 +2,7 @@ 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, @@ -73,7 +74,7 @@ export class GemXRPHandler { } } - public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { + public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { try { const checkCreateRequest: CheckCreate = await createCheck( this.xrpClient, @@ -89,8 +90,16 @@ export class GemXRPHandler { 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: updatedCheckCreateRequest, + transaction: checkCreateRequest, }); if ( @@ -111,11 +120,20 @@ export class GemXRPHandler { console.log(`Response for submitted Transaction Request:`, submitCheckCreateRequestResponse); checkRippleTransactionResult(submitCheckCreateRequestResponse); + + return submitCheckCreateRequestResponse.result.hash; } catch (error) { - throw new Error(`Error creating Check: ${error}`); + throw new Error(`Error signing and submitting Check: ${error}`); } } + public async sendCheckTXHash(coordinatorURL: string, checkTXHash: string): Promise { + try { + await submitXRPLCheckToCash(coordinatorURL, checkTXHash); + } catch (error) { + throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); + } + } public async getDLCBTCBalance(): Promise { try { await connectRippleClient(this.xrpClient); diff --git a/src/network-handlers/xrp-ledger-handler.ts b/src/network-handlers/xrp-ledger-handler.ts index 59a7017..1d05c7a 100644 --- a/src/network-handlers/xrp-ledger-handler.ts +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -2,6 +2,7 @@ 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, @@ -84,7 +85,7 @@ export class LedgerXRPHandler { } } - public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { + public async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise { try { const checkCreateRequest: CheckCreate = await createCheck( this.xrpClient, @@ -101,7 +102,15 @@ export class LedgerXRPHandler { SigningPubKey: this.publicKey.toUpperCase(), }; - const encodedCheckCreateRequest = encode(updatedCheckCreateRequest); + 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, @@ -109,7 +118,7 @@ export class LedgerXRPHandler { ); const signedCheckCreateRequest: Transaction = { - ...updatedCheckCreateRequest, + ...checkCreateRequest, TxnSignature: signature, }; @@ -121,8 +130,18 @@ export class LedgerXRPHandler { console.log(`Response for submitted Transaction Request:`, submitCheckCreateRequestResponse); checkRippleTransactionResult(submitCheckCreateRequestResponse); + + return submitCheckCreateRequestResponse.result.hash; } catch (error) { - throw new Error(`Error creating Check: ${error}`); + throw new Error(`Error signing and submitting Check: ${error}`); + } + } + + public async sendCheckTXHash(coordinatorURL: string, checkTXHash: string): Promise { + try { + await submitXRPLCheckToCash(coordinatorURL, checkTXHash); + } catch (error) { + throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); } } From 857107e2c191f81ab18eabe27589199ed6fe1227 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 21 Oct 2024 16:08:28 +0200 Subject: [PATCH 23/27] feat: add pagination to getAllRippleVaults --- package.json | 2 +- src/functions/ripple/ripple.functions.ts | 54 ++-- src/network-handlers/ripple-handler.ts | 326 ++++++++++------------- 3 files changed, 178 insertions(+), 204 deletions(-) diff --git a/package.json b/package.json index 3f4c08c..63cbfc5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.5", + "version": "2.4.6", "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", diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index ae545b6..f0ffaf0 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -199,30 +199,25 @@ export async function getRippleVault( try { await connectRippleClient(rippleClient); - let formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID.slice(2) : vaultUUID; - formattedUUID = formattedUUID.toUpperCase(); - const getAccountNFTsRequest: AccountNFTsRequest = { - command: 'account_nfts', - account: issuerAddress, - }; - - const { - result: { account_nfts: rippleNFTs }, - } = await rippleClient.request(getAccountNFTsRequest); + // let formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID.slice(2) : vaultUUID; + // formattedUUID = formattedUUID.toUpperCase(); + const formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID : `0x${vaultUUID}`; - const nftID = findNFTByUUID(rippleNFTs, formattedUUID).NFTokenID; + const allVaults = await getAllRippleVaults(rippleClient, issuerAddress); - const matchingNFTs = rippleNFTs.filter(nft => nft.NFTokenID === nftID); + const matchingVaults = allVaults.filter( + vault => vault.uuid.toLowerCase() === formattedUUID.toLowerCase() + ); - if (matchingNFTs.length === 0) { + if (matchingVaults.length === 0) { throw new RippleError(`Vault with UUID: ${formattedUUID} not found`); } - if (matchingNFTs.length > 1) { + if (matchingVaults.length > 1) { throw new RippleError(`Multiple Vaults found with UUID: ${formattedUUID}`); } - return hexFieldsToLowercase(decodeURI(matchingNFTs[0].URI!)); + return matchingVaults[0]; } catch (error) { throw new RippleError(`Error getting Vault ${vaultUUID}: ${error}`); } @@ -236,17 +231,28 @@ export async function getAllRippleVaults( try { await connectRippleClient(rippleClient); - const getAccountNFTsRequest: AccountNFTsRequest = { - command: 'account_nfts', - account: issuerAddress, - limit: 400, - }; + let marker: any = undefined; + const limit = 100; + let allRippleNFTs: any[] = []; - const { - result: { account_nfts: rippleNFTs }, - } = await rippleClient.request(getAccountNFTsRequest); + 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 = rippleNFTs.map(nft => hexFieldsToLowercase(decodeURI(nft.URI!))); + const rippleVaults = allRippleNFTs.map(nft => hexFieldsToLowercase(decodeURI(nft.URI!))); if (ownerAddress) { return rippleVaults.filter(vault => vault.creator === ownerAddress); diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 4d2e252..e5f549a 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -17,7 +17,9 @@ 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'; @@ -28,17 +30,6 @@ interface SignResponse { hash: string; } -function lowercaseHexFields(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(), - }; -} - function buildDefaultNftVault(): RawVault { return { uuid: `0x${'0'.repeat(64)}`, @@ -70,7 +61,7 @@ export class RippleHandler { websocketURL: string, minSigners: number ) { - this.client = new xrpl.Client(websocketURL); + this.client = new xrpl.Client(websocketURL, { timeout: 10000 }); this.wallet = xrpl.Wallet.fromSeed(seedPhrase); this.issuerAddress = issuerAddress; this.minSigners = minSigners; @@ -85,11 +76,18 @@ export class RippleHandler { return new RippleHandler(seedPhrase, issuerAddress, websocketURL, minSigners); } - async submit(signatures: string[]): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); + async disconnectClient(): Promise { + try { + await this.client.disconnect(); + } catch (error) { + throw new RippleError(`Could not disconnect client: ${error}`); } + } + + async submit(signatures: string[]): Promise { try { + await connectRippleClient(this.client); + const multisig_tx = xrpl.multisign(signatures); const tx: xrpl.TxResponse = @@ -106,10 +104,9 @@ export class RippleHandler { } async getNetworkInfo(): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + return await this.client.request({ command: 'server_info' }); } catch (error) { throw new RippleError(`Could not fetch Network Info: ${error}`); @@ -117,10 +114,9 @@ export class RippleHandler { } async getAddress(): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + return this.wallet.classicAddress; } catch (error) { throw new RippleError(`Could not fetch Address Info: ${error}`); @@ -128,37 +124,19 @@ export class RippleHandler { } async getRawVault(uuid: string): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { - const getNFTsTransaction: AccountNFTsRequest = { - command: 'account_nfts', - account: this.issuerAddress, - limit: 400, - }; - let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; - nftUUID = nftUUID.toUpperCase(); - const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - const nftTokenId = await this.getNFTokenIdForVault(nftUUID); - const matchingNFT = nfts.result.account_nfts.filter(nft => nft.NFTokenID === nftTokenId); - if (matchingNFT.length === 0) { - throw new RippleError(`Vault with UUID: ${nftUUID} not found`); - } else if (matchingNFT.length > 1) { - throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`); - } - const matchingVault: RawVault = decodeURI(matchingNFT[0].URI!); - return lowercaseHexFields(matchingVault); + await connectRippleClient(this.client); + + 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): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + const newVault = buildDefaultNftVault(); newVault.uuid = uuid; newVault.creator = userAddress; @@ -173,10 +151,9 @@ export class RippleHandler { // Things like withdraw and deposit should get the existing NFT vault // then burn the NFT, and mint a new one with the updated value // putting the UUID into the URI - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + console.log(`Performing Withdraw for User: ${uuid}`); let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid; nftUUID = nftUUID.toUpperCase(); @@ -196,10 +173,9 @@ export class RippleHandler { mintTokensSignedTxBlobs: string[], // this can be a set of empty string is no tokens are being minted mintNFTSignedTxBlobs: string[] ): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + console.log('Doing the burn for SSF'); const burn_multisig_tx = xrpl.multisign(burnNFTSignedTxBlobs); const burnTx: xrpl.TxResponse = @@ -249,10 +225,9 @@ export class RippleHandler { burnNFTSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + console.log('Doing the check cashing'); // multisig burn const cash_check_tx = xrpl.multisign(cashCheckSignedTxBlobs); @@ -298,10 +273,9 @@ export class RippleHandler { burnNFTSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + console.log('Doing the burn for SSP'); // multisig burn const burn_multisig_tx = xrpl.multisign(burnNFTSignedTxBlobs); @@ -334,32 +308,20 @@ export class RippleHandler { } async getContractVaults(): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { - const getNFTsTransaction: AccountNFTsRequest = { - command: 'account_nfts', - account: this.issuerAddress, - limit: 400, - }; - - const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction); - const allNFTs = nfts.result.account_nfts; - const allVaults: RawVault[] = allNFTs.map(nft => lowercaseHexFields(decodeURI(nft.URI!))); + await connectRippleClient(this.client); - return allVaults; + return await getAllRippleVaults(this.client, this.issuerAddress); } catch (error) { throw new RippleError(`Could not fetch All Vaults: ${error}`); } } async getNFTokenIdForVault(uuid: string): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } console.log(`Getting NFTokenId for vault: ${uuid}`); try { + await connectRippleClient(this.client); + const getNFTsTransaction: AccountNFTsRequest = { command: 'account_nfts', account: this.issuerAddress, @@ -382,9 +344,8 @@ export class RippleHandler { async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { try { - if (!this.client.isConnected()) { - await this.client.connect(); - } + await connectRippleClient(this.client); + console.log(`Getting sig for Burning Ripple Vault, vault: ${nftUUID}`); const nftTokenId = await this.getNFTokenIdForVault(nftUUID); const burnTransactionJson: SubmittableTransaction = { @@ -415,9 +376,8 @@ export class RippleHandler { async mintNFT(vault: RawVault, incrementBy: number = 0): Promise { try { - if (!this.client.isConnected()) { - await this.client.connect(); - } + await connectRippleClient(this.client); + console.log(`Getting sig for Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`); const newURI = encodeURI(vault); console.log('newURI: ', newURI); @@ -448,10 +408,9 @@ export class RippleHandler { } async getSigUpdateVaultForSSP(uuid: string, updates: SSPVaultUpdate): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + console.log(`Getting sig for getSigUpdateVaultForSSP, vault uuid: ${uuid}`); const nftUUID = uuid; const thisVault = await this.getRawVault(nftUUID); @@ -474,10 +433,9 @@ export class RippleHandler { updates: SSFVaultUpdate, updateSequenceBy: number ): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } try { + await connectRippleClient(this.client); + const nftUUID = uuid; const thisVault = await this.getRawVault(nftUUID); const updatedVault = { @@ -495,26 +453,30 @@ export class RippleHandler { } async getAllChecks(): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } + try { + await connectRippleClient(this.client); - const getAccountObjectsRequestJSON: Request = { - command: 'account_objects', - account: this.issuerAddress, - ledger_index: 'validated', - type: 'check', - }; + const getAccountObjectsRequestJSON: Request = { + command: 'account_objects', + account: this.issuerAddress, + ledger_index: 'validated', + type: 'check', + }; - const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request( - getAccountObjectsRequestJSON - ); + const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request( + getAccountObjectsRequestJSON + ); - return getAccountObjectsResponse.result.account_objects; + return getAccountObjectsResponse.result.account_objects; + } catch (error) { + throw new RippleError(`Could not fetch Checks: ${error}`); + } } async getCashCheckAndWithdrawSignatures(txHash: string): Promise { try { + await connectRippleClient(this.client); + const check = await getCheckByTXHash(this.client, this.issuerAddress, txHash); const invoiceID = check.InvoiceID; @@ -543,42 +505,46 @@ export class RippleHandler { } async cashCheck(checkID: string, dlcBTCAmount: string): Promise { - await connectRippleClient(this.client); - - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - 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; + try { + await connectRippleClient(this.client); + + 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + 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( @@ -587,55 +553,57 @@ export class RippleHandler { valueMinted: number, incrementBy: number = 0 ): Promise { - if (!this.client.isConnected()) { - await this.client.connect(); - } + try { + await connectRippleClient(this.client); - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - updatedSendTokenTransactionJSON.LastLedgerSequence = - Math.ceil(updatedSendTokenTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; - - if (incrementBy > 0) { - updatedSendTokenTransactionJSON.Sequence = - updatedSendTokenTransactionJSON.Sequence! + incrementBy; - } + 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`); - console.log( - 'Issuer is about to sign the following mintTokens tx: ', - updatedSendTokenTransactionJSON - ); + const sendTokenTransactionJSON: Payment = { + TransactionType: 'Payment', + Account: this.issuerAddress, + Destination: destinationAddress, + DestinationTag: 1, + Amount: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + value: dlcBTCAmount, + issuer: this.issuerAddress, + }, + }; - const signSendTokenTransactionResponse: SignResponse = this.wallet.sign( - updatedSendTokenTransactionJSON, - true - ); + const updatedSendTokenTransactionJSON: Payment = await this.client.autofill( + sendTokenTransactionJSON, + this.minSigners + ); - return signSendTokenTransactionResponse.tx_blob; + // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 + // 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 + 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}`); + } } } From 98b2804e4dd17e64b48852ffa887743ab299b5d0 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 22 Oct 2024 12:02:08 +0200 Subject: [PATCH 24/27] feat: add fee arguments to setup vault --- package.json | 2 +- src/network-handlers/ripple-handler.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 63cbfc5..59d2b0b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.6", + "version": "2.4.7", "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", diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index e5f549a..497db17 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -133,7 +133,13 @@ export class RippleHandler { } } - async setupVault(uuid: string, userAddress: string, timeStamp: number): Promise { + async setupVault( + uuid: string, + userAddress: string, + timeStamp: number, + btcMintFeeBasisPoints: number, + btcRedeemFeeBasisPoints: number + ): Promise { try { await connectRippleClient(this.client); @@ -141,6 +147,8 @@ export class RippleHandler { 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}`); From a8c2ca8d76fa85a1a46d813eb645c7190714ec79 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 23 Oct 2024 13:58:45 +0200 Subject: [PATCH 25/27] feat: add attestorChainID to xrpl attestor requests --- .gitignore | 3 --- package.json | 2 +- .../attestor/attestor-request.functions.ts | 17 +++++++++++++---- src/models/attestor.models.ts | 11 ++++++++++- src/network-handlers/xrp-gem-wallet-handler.ts | 9 +++++++-- src/network-handlers/xrp-ledger-handler.ts | 9 +++++++-- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index a138e33..2b3f55c 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,3 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* - -# Local Netlify folder -.netlify diff --git a/package.json b/package.json index 59d2b0b..5f4f986 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.7", + "version": "2.4.8", "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", diff --git a/src/functions/attestor/attestor-request.functions.ts b/src/functions/attestor/attestor-request.functions.ts index e3c07c3..f4026e0 100644 --- a/src/functions/attestor/attestor-request.functions.ts +++ b/src/functions/attestor/attestor-request.functions.ts @@ -1,6 +1,7 @@ import { equals, filter, isEmpty, isNotNil, join, map, prop } from 'ramda'; import { + AttestorChainID, FundingTXAttestorInfo, WithdrawDepositTXAttestorInfo, } from '../../models/attestor.models.js'; @@ -9,14 +10,22 @@ import { sendGetRequest, sendRequest } from '../request/request.functions.js'; export async function submitSetupXRPLVaultRequest( coordinatorURL: string, - userXRPLAddress: string + userXRPLAddress: string, + attestorChainID: AttestorChainID ): Promise { - const requestBody = JSON.stringify({ user_xrpl_address: userXRPLAddress }); + 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): Promise { - const requestBody = JSON.stringify({ tx_hash: txHash }); +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); } diff --git a/src/models/attestor.models.ts b/src/models/attestor.models.ts index bd1fe88..4a5e247 100644 --- a/src/models/attestor.models.ts +++ b/src/models/attestor.models.ts @@ -1,11 +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' | 'ripple-xrpl-mainnet' - | 'ripple-xrpl-testnet'; + | 'ripple-xrpl-testnet' + | 'ripple-xrpl-devnet'; export interface FundingTXAttestorInfo { vaultUUID: string; diff --git a/src/network-handlers/xrp-gem-wallet-handler.ts b/src/network-handlers/xrp-gem-wallet-handler.ts index a7337c0..3f5ff56 100644 --- a/src/network-handlers/xrp-gem-wallet-handler.ts +++ b/src/network-handlers/xrp-gem-wallet-handler.ts @@ -11,6 +11,7 @@ import { getLockedBTCBalance, setTrustLine, } from '../functions/ripple/ripple.functions.js'; +import { AttestorChainID } from '../models/attestor.models.js'; export class GemXRPHandler { private xrpClient: Client; @@ -127,9 +128,13 @@ export class GemXRPHandler { } } - public async sendCheckTXHash(coordinatorURL: string, checkTXHash: string): Promise { + public async sendCheckTXHash( + coordinatorURL: string, + checkTXHash: string, + attestorChainID: AttestorChainID + ): Promise { try { - await submitXRPLCheckToCash(coordinatorURL, checkTXHash); + await submitXRPLCheckToCash(coordinatorURL, checkTXHash, attestorChainID); } catch (error) { throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); } diff --git a/src/network-handlers/xrp-ledger-handler.ts b/src/network-handlers/xrp-ledger-handler.ts index 1d05c7a..17deb1b 100644 --- a/src/network-handlers/xrp-ledger-handler.ts +++ b/src/network-handlers/xrp-ledger-handler.ts @@ -11,6 +11,7 @@ import { getLockedBTCBalance, setTrustLine, } from '../functions/ripple/ripple.functions.js'; +import { AttestorChainID } from '../models/attestor.models.js'; export class LedgerXRPHandler { private ledgerApp: Xrp.default; @@ -137,9 +138,13 @@ export class LedgerXRPHandler { } } - public async sendCheckTXHash(coordinatorURL: string, checkTXHash: string): Promise { + public async sendCheckTXHash( + coordinatorURL: string, + checkTXHash: string, + attestorChainID: AttestorChainID + ): Promise { try { - await submitXRPLCheckToCash(coordinatorURL, checkTXHash); + await submitXRPLCheckToCash(coordinatorURL, checkTXHash, attestorChainID); } catch (error) { throw new Error(`Error sending Check TX Hash to Attestors: ${error}`); } From 3159e781be066b3bf3a913a61230868782d9449a Mon Sep 17 00:00:00 2001 From: sosaucily Date: Sun, 27 Oct 2024 12:41:23 +0100 Subject: [PATCH 26/27] use a withXrplConnection context for functions that need a connection --- src/functions/ripple/ripple.functions.ts | 7 +- src/network-handlers/ripple-handler.ts | 851 ++++++++++++----------- 2 files changed, 442 insertions(+), 416 deletions(-) diff --git a/src/functions/ripple/ripple.functions.ts b/src/functions/ripple/ripple.functions.ts index f0ffaf0..30ce9ea 100644 --- a/src/functions/ripple/ripple.functions.ts +++ b/src/functions/ripple/ripple.functions.ts @@ -130,11 +130,12 @@ export function getRippleWallet(seedPhrase: string): Wallet { return Wallet.fromSeed(seedPhrase); } -export async function connectRippleClient(rippleClient: Client): Promise { +export async function connectRippleClient(rippleClient: Client): Promise { if (rippleClient.isConnected()) { - return; + return false; } await rippleClient.connect(); + return true; } export function formatRippleVaultUUID(vaultUUID: string): string { @@ -199,8 +200,6 @@ export async function getRippleVault( try { await connectRippleClient(rippleClient); - // let formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID.slice(2) : vaultUUID; - // formattedUUID = formattedUUID.toUpperCase(); const formattedUUID = vaultUUID.substring(0, 2) === '0x' ? vaultUUID : `0x${vaultUUID}`; const allVaults = await getAllRippleVaults(rippleClient, issuerAddress); diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index 497db17..fc0d472 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -76,47 +76,58 @@ export class RippleHandler { return new RippleHandler(seedPhrase, issuerAddress, websocketURL, minSigners); } - async disconnectClient(): Promise { + async withConnectionMgmt(callback: () => Promise): Promise { + console.log('Connecting to the async service...'); + const newConnection = !this.client.isConnected(); try { - await this.client.disconnect(); - } catch (error) { - throw new RippleError(`Could not disconnect client: ${error}`); + 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 { - try { - await connectRippleClient(this.client); - - const multisig_tx = xrpl.multisign(signatures); + 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; + 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}`); + 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}`); } - return tx.result.hash; - } catch (error) { - throw new RippleError(`Could not submit transaction: ${error}`); - } + }); } async getNetworkInfo(): Promise { - try { - await connectRippleClient(this.client); - - return await this.client.request({ command: 'server_info' }); - } catch (error) { - throw new RippleError(`Could not fetch Network Info: ${error}`); - } + 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 { - await connectRippleClient(this.client); - return this.wallet.classicAddress; } catch (error) { throw new RippleError(`Could not fetch Address Info: ${error}`); @@ -124,13 +135,13 @@ export class RippleHandler { } async getRawVault(uuid: string): Promise { - try { - await connectRippleClient(this.client); - - return await getRippleVault(this.client, this.issuerAddress, uuid); - } catch (error) { - throw new RippleError(`Could not fetch Vault: ${error}`); - } + 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( @@ -140,92 +151,90 @@ export class RippleHandler { btcMintFeeBasisPoints: number, btcRedeemFeeBasisPoints: number ): Promise { - try { - await connectRippleClient(this.client); - - 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}`); - } + 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 { - // Things like withdraw and deposit should get the existing NFT vault - // then burn the NFT, and mint a new one with the updated value - // putting the UUID into the URI - try { - await connectRippleClient(this.client); - - 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}`); - } + 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[], // this can be a set of empty string is no tokens are being minted + mintTokensSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { - try { - await connectRippleClient(this.client); - - 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}` - ); - } + 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 $$'); + // 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'); + } - 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') { + 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 tokens to user: ${mintTokenMeta!.TransactionResult}` + `Could not mint temporary Ripple Vault: ${mintMeta!.TransactionResult}` ); } - } else { - console.log('No need to mint tokens, because this was a withdraw flow SSF'); + } catch (error) { + throw new RippleError(`Unable to set Vault status to FUNDED: ${error}`); } - - 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( @@ -233,207 +242,215 @@ export class RippleHandler { burnNFTSignedTxBlobs: string[], mintNFTSignedTxBlobs: string[] ): Promise { - try { - await connectRippleClient(this.client); + 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 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('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'); - 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}` + ); + } - // 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}`); } - - 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 { - try { - await connectRippleClient(this.client); + 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('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'); - 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}` + ); + } - // 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}`); } - - 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 { - try { - await connectRippleClient(this.client); - - return await getAllRippleVaults(this.client, this.issuerAddress); - } catch (error) { - throw new RippleError(`Could not fetch All Vaults: ${error}`); - } + 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 { - console.log(`Getting NFTokenId for vault: ${uuid}`); - try { - await connectRippleClient(this.client); - - 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 - ); + 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`); + 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}`); } - return matchingNFT.NFTokenID; - } catch (error) { - throw new RippleError(`Could not find NFTokenId for vault Vault: ${error}`); - } + }); } async burnNFT(nftUUID: string, incrementBy: number = 0): Promise { - try { - await connectRippleClient(this.client); - - 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); // this hardcoded number should match the number of active signers - - // set the LastLedgerSequence to equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - preparedBurnTx.LastLedgerSequence = - Math.ceil(preparedBurnTx.LastLedgerSequence! / 10000 + 1) * 10000; // Better way?!? - - if (incrementBy > 0) { - preparedBurnTx.Sequence = preparedBurnTx.Sequence! + incrementBy; - } + 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); + 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}`); - } + 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 { - try { - await connectRippleClient(this.client); - - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - preparedMintTx.LastLedgerSequence = - Math.ceil(preparedMintTx.LastLedgerSequence! / 10000 + 1) * 10000; - if (incrementBy > 0) { - preparedMintTx.Sequence = preparedMintTx.Sequence! + incrementBy; - } + 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); + 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}`); - } + 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 { - try { - await connectRippleClient(this.client); - - 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}`); - } + 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( @@ -441,118 +458,123 @@ export class RippleHandler { updates: SSFVaultUpdate, updateSequenceBy: number ): Promise { - try { - await connectRippleClient(this.client); - - 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}`); - } + 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 { - try { - await connectRippleClient(this.client); - - const getAccountObjectsRequestJSON: Request = { - command: 'account_objects', - account: this.issuerAddress, - ledger_index: 'validated', - type: 'check', - }; - - const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request( - getAccountObjectsRequestJSON - ); + 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}`); - } + return getAccountObjectsResponse.result.account_objects; + } catch (error) { + throw new RippleError(`Could not fetch Checks: ${error}`); + } + }); } async getCashCheckAndWithdrawSignatures(txHash: string): Promise { - try { - await connectRippleClient(this.client); - - const check = await getCheckByTXHash(this.client, this.issuerAddress, txHash); - const invoiceID = check.InvoiceID; + 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}`); - } + if (!invoiceID) { + throw new RippleError(`Could not find Invoice ID for Check with TX Hash: ${txHash}`); + } - const vault = await this.getRawVault(`0x${invoiceID}`.toLowerCase()); + const vault = await this.getRawVault(`0x${invoiceID}`.toLowerCase()); - if (!vault) { - throw new RippleError(`Could not find Vault for Check with Invoice ID: ${check.InvoiceID}`); - } + if (!vault) { + throw new RippleError( + `Could not find Vault for Check with Invoice ID: ${check.InvoiceID}` + ); + } - const checkSendMax = check.SendMax as IssuedCurrencyAmount; + const checkSendMax = check.SendMax as IssuedCurrencyAmount; - const checkCashSignatures = await this.cashCheck(check.index, checkSendMax.value); + 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}`); - } + 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 { - try { - await connectRippleClient(this.client); + 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 + ); - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - 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}`); - } + // 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( @@ -561,57 +583,62 @@ export class RippleHandler { valueMinted: number, incrementBy: number = 0 ): Promise { - try { - await connectRippleClient(this.client); + 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 + ); - 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 equal LastLedgerSequence plus 5 and then rounded up to the nearest 10 - // 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 - updatedSendTokenTransactionJSON.LastLedgerSequence = - Math.ceil(updatedSendTokenTransactionJSON.LastLedgerSequence! / 10000 + 1) * 10000; - - if (incrementBy > 0) { - updatedSendTokenTransactionJSON.Sequence = - updatedSendTokenTransactionJSON.Sequence! + incrementBy; - } + // 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 - ); + console.log( + 'Issuer is about to sign the following mintTokens tx: ', + updatedSendTokenTransactionJSON + ); - const signSendTokenTransactionResponse: SignResponse = this.wallet.sign( - updatedSendTokenTransactionJSON, - true - ); + const signSendTokenTransactionResponse: SignResponse = this.wallet.sign( + updatedSendTokenTransactionJSON, + true + ); - return signSendTokenTransactionResponse.tx_blob; - } catch (error) { - throw new RippleError(`Could not mint tokens: ${error}`); - } + return signSendTokenTransactionResponse.tx_blob; + } catch (error) { + throw new RippleError(`Could not mint tokens: ${error}`); + } + }); } } From 1866b3b26d5f8eae0df0b66421d7af28b0e0e2f0 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 28 Oct 2024 11:32:01 +0100 Subject: [PATCH 27/27] feat: remove lint-commit git hook, bump version --- .github/workflows/code-checks.yml | 10 ---------- package.json | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) 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 5f4f986..3554cf3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "dlc-btc-lib", - "version": "2.4.8", + "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",