From 840f57da0d2fe3cc52d37670becd76ecd8b2cdca Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 11 Jun 2024 17:31:14 -0400 Subject: [PATCH] Adds `AssembledTransaction.toXDR|fromXDR` and `Client.txFromXDR` methods Adds a toXDR and fromXDR for AssembledTransaction class. Also adds a txFromXDR to the Client class. Changes the swap test to use this flow. Limitations: If you use the XDR multi-auth flow, you must re-simulate before the final `signAndSend` call. This is due to the fact that we can't serialize the XDR of the transaction envelope with the results of the simulation. If we want to do this, we would need to add an `AssembledTransaction` type to XDR in `stellar-base`. --------- Co-authored-by: George --- CHANGELOG.md | 12 ++++++ src/contract/assembled_transaction.ts | 58 +++++++++++++++++++++++---- src/contract/client.ts | 3 ++ test/e2e/src/test-swap.js | 13 +++--- 4 files changed, 73 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3508b009..151a3761f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ A breaking change will get clearly marked in this log. ## Unreleased +### Added +- `contract.AssembledTransaction` now has a `toXDR` and `fromXDR` method for serializing the +transaction to and from XDR. Additionally, `contract.Client` now has a `txFromXDR`. These methods +should be used in place of `AssembledTransaction.toJSON`, `AssembledTransaction.fromJSON`, and +`Client.txFromJSON` for multi-auth signing. The JSON methods are now deprecated. **Note you must now +call `simulate` on the transaction before the final `signAndSend` call after all required signatures +are gathered when using the XDR methods. + +### Deprecated +- In `contract.AssembledTransaction`, `toJSON` and `fromJSON` should be replaced with `toXDR` and +`fromXDR`. Similarly, in `contract.Client`, `txFromJSON` should be replaced with `txFromXDR`. + ## [v12.0.1](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.1) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 1f46824b9..de584fb81 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -28,6 +28,7 @@ import { implementsToString, } from "./utils"; import { SentTransaction } from "./sent_transaction"; +import { Spec } from "./spec"; export const NULL_ACCOUNT = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; @@ -364,6 +365,47 @@ export class AssembledTransaction { return txn; } + /** + * Serialize the AssembledTransaction to a base64-encoded XDR string. + */ + toXDR(): string { + if(!this.built) throw new Error( + "Transaction has not yet been simulated; " + + "call `AssembledTransaction.simulate` first.", + ); + return this.built?.toEnvelope().toXDR('base64'); + } + + /** + * Deserialize the AssembledTransaction from a base64-encoded XDR string. + */ + static fromXDR( + options: Omit, "args" | "method" | "parseResultXdr">, + encodedXDR: string, + spec: Spec + ): AssembledTransaction { + const envelope = xdr.TransactionEnvelope.fromXDR(encodedXDR, "base64"); + const built = TransactionBuilder.fromXDR(envelope, options.networkPassphrase) as Tx; + const operation = built.operations[0] as Operation.InvokeHostFunction; + if (!operation?.func?.value || typeof operation.func.value !== 'function') { + throw new Error("Could not extract the method from the transaction envelope."); + } + const invokeContractArgs = operation.func.value() as xdr.InvokeContractArgs; + if (!invokeContractArgs?.functionName) { + throw new Error("Could not extract the method name from the transaction envelope."); + } + const method = invokeContractArgs.functionName().toString('utf-8'); + const txn = new AssembledTransaction( + { ...options, + method, + parseResultXdr: (result: xdr.ScVal) => + spec.funcResToNative(method, result), + } + ); + txn.built = built; + return txn; + } + private constructor(public options: AssembledTransactionOptions) { this.options.simulate = this.options.simulate ?? true; this.server = new Server(this.options.rpcUrl, { @@ -412,14 +454,16 @@ export class AssembledTransaction { } simulate = async (): Promise => { - if (!this.raw) { - throw new Error( - "Transaction has not yet been assembled; " + - "call `AssembledTransaction.build` first.", - ); - } + if (!this.built) { + if (!this.raw) { + throw new Error( + "Transaction has not yet been assembled; " + + "call `AssembledTransaction.build` first.", + ); + } - this.built = this.raw.build(); + this.built = this.raw.build(); + } this.simulation = await this.server.simulateTransaction(this.built); if (Api.isSimulationSuccess(this.simulation)) { diff --git a/src/contract/client.ts b/src/contract/client.ts index e0c298b4a..f576762a6 100644 --- a/src/contract/client.ts +++ b/src/contract/client.ts @@ -124,5 +124,8 @@ export class Client { tx, ); }; + + txFromXDR = (xdrBase64: string): AssembledTransaction => AssembledTransaction.fromXDR(this.options, xdrBase64, this.spec); + } diff --git a/test/e2e/src/test-swap.js b/test/e2e/src/test-swap.js index 129c181be..71e0e0ab4 100644 --- a/test/e2e/src/test-swap.js +++ b/test/e2e/src/test-swap.js @@ -100,31 +100,32 @@ describe("Swap Contract Tests", function () { expect(needsNonInvokerSigningBy.indexOf(this.context.bob.publicKey())).to.equal(1, "needsNonInvokerSigningBy does not have bob's public key!"); // root serializes & sends to alice - const jsonFromRoot = tx.toJSON(); + const xdrFromRoot = tx.toXDR(); const { client: clientAlice } = await clientFor("swap", { keypair: this.context.alice, contractId: this.context.swapId, }); - const txAlice = clientAlice.txFromJSON(jsonFromRoot); + const txAlice = clientAlice.txFromXDR(xdrFromRoot); await txAlice.signAuthEntries(); // alice serializes & sends to bob - const jsonFromAlice = txAlice.toJSON(); + const xdrFromAlice = txAlice.toXDR(); const { client: clientBob } = await clientFor("swap", { keypair: this.context.bob, contractId: this.context.swapId, }); - const txBob = clientBob.txFromJSON(jsonFromAlice); + const txBob = clientBob.txFromXDR(xdrFromAlice); await txBob.signAuthEntries(); // bob serializes & sends back to root - const jsonFromBob = txBob.toJSON(); + const xdrFromBob = txBob.toXDR(); const { client: clientRoot } = await clientFor("swap", { keypair: this.context.root, contractId: this.context.swapId, }); - const txRoot = clientRoot.txFromJSON(jsonFromBob); + const txRoot = clientRoot.txFromXDR(xdrFromBob); + await txRoot.simulate(); const result = await txRoot.signAndSend(); expect(result).to.have.property('sendTransactionResponse');