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');