diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts index d6751d7..4a05186 100644 --- a/src/network-handlers/ripple-handler.ts +++ b/src/network-handlers/ripple-handler.ts @@ -225,7 +225,6 @@ export class RippleHandler { preparedCreateTicketTransaction, true ).tx_blob; - console.log('createTicketTransactionSignature: ', createTicketTransactionSignature); return [ { diff --git a/updates.patch b/updates.patch new file mode 100644 index 0000000..a490a22 --- /dev/null +++ b/updates.patch @@ -0,0 +1,414 @@ +diff --git a/package.json b/package.json +index a4dd307..fc9d05c 100644 +--- a/package.json ++++ b/package.json +@@ -1,7 +1,7 @@ + { + "type": "module", + "name": "dlc-btc-lib", +- "version": "2.4.18", ++ "version": "2.4.19", + "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 644758b..f52caf5 100644 +--- a/src/functions/ripple/ripple.functions.ts ++++ b/src/functions/ripple/ripple.functions.ts +@@ -10,6 +10,7 @@ import { + Client, + LedgerEntry, + SubmittableTransaction, ++ TicketCreate, + Transaction, + TransactionMetadataBase, + TrustSet, +@@ -17,6 +18,7 @@ import { + Wallet, + convertHexToString, + convertStringToHex, ++ multisign, + } from 'xrpl'; + + import { +@@ -405,3 +407,38 @@ export async function getCheckByTXHash( + throw new RippleError(`Error getting Check by TX Hash: ${error}`); + } + } ++ ++export async function createTicket( ++ xrplClient: Client, ++ accountAddress: string, ++ ticketAmount: number, ++ signerQuorum?: number ++): Promise { ++ const createTicketRequest: TicketCreate = { ++ TransactionType: 'TicketCreate', ++ Account: accountAddress, ++ TicketCount: ticketAmount, ++ }; ++ ++ const updatedCreateTicketRequest = await xrplClient.autofill(createTicketRequest, signerQuorum); ++ return updatedCreateTicketRequest; ++} ++ ++export async function signTransaction( ++ xrplWallet: Wallet, ++ transaction: SubmittableTransaction ++): Promise { ++ try { ++ return xrplWallet.sign(transaction); ++ } catch (error) { ++ throw new RippleError(`Error signing Create Ticket: ${error}`); ++ } ++} ++ ++export function multiSignTransaction(signedTransactions: string[]): string { ++ try { ++ return multisign(signedTransactions); ++ } catch (error) { ++ throw new RippleError(`Error multi-signing Transaction: ${error}`); ++ } ++} +diff --git a/src/models/ripple.model.ts b/src/models/ripple.model.ts +index 63aa179..980d857 100644 +--- a/src/models/ripple.model.ts ++++ b/src/models/ripple.model.ts +@@ -10,7 +10,7 @@ export interface AutoFillValues { + Fee: string; + } + +-export type SignatureType = 'cashCheck' | 'burnNFT' | 'mintNFT' | 'mintToken'; ++export type SignatureType = 'cashCheck' | 'burnNFT' | 'mintNFT' | 'mintToken' | 'createTicket'; + + export interface XRPLSignatures { + signatureType: SignatureType; +diff --git a/src/network-handlers/ripple-handler.ts b/src/network-handlers/ripple-handler.ts +index 37485f0..d6751d7 100644 +--- a/src/network-handlers/ripple-handler.ts ++++ b/src/network-handlers/ripple-handler.ts +@@ -5,21 +5,26 @@ import xrpl, { + AccountObject, + AccountObjectsResponse, + CheckCash, ++ CreatedNode, + IssuedCurrencyAmount, + Payment, + Request, + SubmittableTransaction, ++ TicketCreate, ++ TransactionMetadata, + } from 'xrpl'; + import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js'; + + import { XRPL_DLCBTC_CURRENCY_HEX } from '../constants/ripple.constants.js'; + import { ++ checkRippleTransactionResult, + connectRippleClient, + decodeURI, + encodeURI, + getAllRippleVaults, + getCheckByTXHash, + getRippleVault, ++ multiSignTransaction, + } from '../functions/ripple/ripple.functions.js'; + import { RippleError } from '../models/errors.js'; + import { RawVault, SSFVaultUpdate, SSPVaultUpdate } from '../models/ethereum-models.js'; +@@ -150,12 +155,102 @@ export class RippleHandler { + }); + } + ++ async submitCreateTicketTransaction(xrplSignatures: XRPLSignatures[]): Promise { ++ return await this.withConnectionMgmt(async () => { ++ try { ++ const signedTransactionBlobs = xrplSignatures.find( ++ sig => sig.signatureType === 'createTicket' ++ )!.signatures; ++ const multisignedTransaction = multiSignTransaction(signedTransactionBlobs); ++ ++ const submitCreateTicketTransactionResponse = ++ await this.client.submitAndWait(multisignedTransaction); ++ ++ checkRippleTransactionResult(submitCreateTicketTransactionResponse); ++ ++ const meta = submitCreateTicketTransactionResponse.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 createdNodes = (meta as TransactionMetadata).AffectedNodes.filter( ++ (node): node is CreatedNode => 'CreatedNode' in node ++ ).map(node => node.CreatedNode); ++ ++ return createdNodes.map(node => node.NewFields.TicketSequence) as string[]; ++ } catch (error) { ++ throw new RippleError(`Could not submit Ticket Transaction: ${error}`); ++ } ++ }); ++ } ++ ++ async createTicket( ++ ticketCount: number, ++ autoFillValues?: AutoFillValues ++ ): Promise { ++ return await this.withConnectionMgmt(async () => { ++ try { ++ const createTicketRequest: TicketCreate = { ++ TransactionType: 'TicketCreate', ++ Account: this.issuerAddress, ++ TicketCount: ticketCount, ++ }; ++ ++ const preparedCreateTicketTransaction = await this.client.autofill( ++ createTicketRequest, ++ this.minSigners ++ ); ++ if (autoFillValues) { ++ preparedCreateTicketTransaction.Fee = autoFillValues.Fee; ++ preparedCreateTicketTransaction.LastLedgerSequence = autoFillValues.LastLedgerSequence; ++ preparedCreateTicketTransaction.Sequence = autoFillValues.Sequence; ++ } else { ++ // 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 ++ preparedCreateTicketTransaction.LastLedgerSequence = ++ Math.ceil(preparedCreateTicketTransaction.LastLedgerSequence! / 10000 + 1) * 10000; ++ } ++ ++ console.log('preparedCreateTicketTransaction ', preparedCreateTicketTransaction); ++ ++ const createTicketTransactionSignature = this.wallet.sign( ++ preparedCreateTicketTransaction, ++ true ++ ).tx_blob; ++ console.log('createTicketTransactionSignature: ', createTicketTransactionSignature); ++ ++ return [ ++ { ++ tx_blob: createTicketTransactionSignature, ++ autoFillValues: { ++ signatureType: 'createTicket', ++ LastLedgerSequence: preparedCreateTicketTransaction.LastLedgerSequence!, ++ Sequence: preparedCreateTicketTransaction.Sequence!, ++ Fee: preparedCreateTicketTransaction.Fee!, ++ }, ++ }, ++ ]; ++ } catch (error) { ++ throw new RippleError(`Could not create Ticket: ${error}`); ++ } ++ }); ++ } ++ + async setupVault( + uuid: string, + userAddress: string, + timeStamp: number, + btcMintFeeBasisPoints: number, + btcRedeemFeeBasisPoints: number, ++ ticket: string, + autoFillValues?: AutoFillValues[] + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -170,7 +265,7 @@ export class RippleHandler { + return [ + await this.mintNFT( + newVault, +- undefined, ++ ticket, + autoFillValues?.find(sig => sig.signatureType === 'mintNFT') + ), + ]; +@@ -183,6 +278,7 @@ export class RippleHandler { + async withdraw( + uuid: string, + withdrawAmount: bigint, ++ tickets: string[], + autoFillValues: AutoFillValues[] + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -194,14 +290,14 @@ export class RippleHandler { + const thisVault = await this.getRawVault(nftUUID); + const burnSig = await this.burnNFT( + nftUUID, +- 1, ++ tickets[0], + autoFillValues.find(sig => sig.signatureType === 'burnNFT') + ); + + thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount)); + const mintSig = await this.mintNFT( + thisVault, +- 2, ++ tickets[1], + autoFillValues.find(sig => sig.signatureType === 'mintNFT') + ); + return [burnSig, mintSig]; +@@ -393,7 +489,7 @@ export class RippleHandler { + + async burnNFT( + nftUUID: string, +- incrementBy: number = 0, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -402,6 +498,8 @@ export class RippleHandler { + const nftTokenId = await this.getNFTokenIdForVault(nftUUID); + const burnTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenBurn', ++ TicketSequence: new Decimal(ticket).toNumber(), ++ Sequence: 0, + Account: this.issuerAddress, + NFTokenID: nftTokenId, + }; +@@ -420,10 +518,6 @@ export class RippleHandler { + // 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); +@@ -448,7 +542,7 @@ export class RippleHandler { + + async mintNFT( + vault: RawVault, +- incrementBy: number = 0, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -460,6 +554,8 @@ export class RippleHandler { + console.log('newURI: ', newURI); + const mintTransactionJson: SubmittableTransaction = { + TransactionType: 'NFTokenMint', ++ TicketSequence: new Decimal(ticket).toNumber(), ++ Sequence: 0, + Account: this.issuerAddress, + URI: newURI, + NFTokenTaxon: 0, +@@ -479,9 +575,6 @@ export class RippleHandler { + // 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); +@@ -505,6 +598,7 @@ export class RippleHandler { + async getSigUpdateVaultForSSP( + uuid: string, + updates: SSPVaultUpdate, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -520,7 +614,7 @@ export class RippleHandler { + taprootPubKey: updates.taprootPubKey, + }; + console.log(`the updated vault: `, updatedVault); +- return await this.mintNFT(updatedVault, 1, autoFillValues); ++ return await this.mintNFT(updatedVault, ticket, autoFillValues); + } catch (error) { + throw new RippleError(`Could not update Vault: ${error}`); + } +@@ -530,7 +624,7 @@ export class RippleHandler { + async getSigUpdateVaultForSSF( + uuid: string, + updates: SSFVaultUpdate, +- updateSequenceBy: number, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -545,7 +639,7 @@ export class RippleHandler { + valueMinted: BigNumber.from(updates.valueMinted), + valueLocked: BigNumber.from(updates.valueLocked), + }; +- return await this.mintNFT(updatedVault, updateSequenceBy, autoFillValues); ++ return await this.mintNFT(updatedVault, ticket, autoFillValues); + } catch (error) { + throw new RippleError(`Could not update Vault: ${error}`); + } +@@ -575,6 +669,7 @@ export class RippleHandler { + + async getCashCheckAndWithdrawSignatures( + txHash: string, ++ tickets: string[], + autoFillValues?: AutoFillValues[] + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -599,12 +694,14 @@ export class RippleHandler { + const checkCashSignatures = await this.cashCheck( + check.index, + checkSendMax.value, ++ tickets[0], + autoFillValues?.find(sig => sig.signatureType === 'cashCheck') + ); + + const mintAndBurnSignatures = await this.withdraw( + vault.uuid, + BigInt(shiftValue(Number(checkSendMax.value))), ++ tickets.slice(1), + autoFillValues ?? [] + ); + return [checkCashSignatures, ...mintAndBurnSignatures]; +@@ -617,6 +714,7 @@ export class RippleHandler { + async cashCheck( + checkID: string, + dlcBTCAmount: string, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -627,6 +725,8 @@ export class RippleHandler { + TransactionType: 'CheckCash', + Account: this.issuerAddress, + CheckID: checkID, ++ TicketSequence: new Decimal(ticket).toNumber(), ++ Sequence: 0, + Amount: { + currency: XRPL_DLCBTC_CURRENCY_HEX, + value: dlcBTCAmount, +@@ -679,7 +779,7 @@ export class RippleHandler { + updatedValueMinted: number, + destinationAddress: string, + valueMinted: number, +- incrementBy: number = 0, ++ ticket: string, + autoFillValues?: AutoFillValues + ): Promise { + return await this.withConnectionMgmt(async () => { +@@ -697,6 +797,8 @@ export class RippleHandler { + const sendTokenTransactionJSON: Payment = { + TransactionType: 'Payment', + Account: this.issuerAddress, ++ TicketSequence: new Decimal(ticket).toNumber(), ++ Sequence: 0, + Destination: destinationAddress, + DestinationTag: 1, + Amount: { +@@ -723,10 +825,6 @@ export class RippleHandler { + // https://xrpl.org/docs/concepts/accounts/tickets + preparedSendTokenTx.LastLedgerSequence = + Math.ceil(preparedSendTokenTx.LastLedgerSequence! / 10000 + 1) * 10000; +- +- if (incrementBy > 0) { +- preparedSendTokenTx.Sequence = preparedSendTokenTx.Sequence! + incrementBy; +- } + } + + console.log('Issuer is about to sign the following mintTokens tx: ', preparedSendTokenTx);