Skip to content

Commit

Permalink
Adds AssembledTransaction.toXDR|fromXDR and Client.txFromXDR methods
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
BlaineHeffron and Shaptic committed Jun 11, 2024
1 parent 7f67469 commit 840f57d
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 13 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 51 additions & 7 deletions src/contract/assembled_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
implementsToString,
} from "./utils";
import { SentTransaction } from "./sent_transaction";
import { Spec } from "./spec";

export const NULL_ACCOUNT =
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
Expand Down Expand Up @@ -364,6 +365,47 @@ export class AssembledTransaction<T> {
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<T>(
options: Omit<AssembledTransactionOptions<T>, "args" | "method" | "parseResultXdr">,
encodedXDR: string,
spec: Spec
): AssembledTransaction<T> {
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<T>) {
this.options.simulate = this.options.simulate ?? true;
this.server = new Server(this.options.rpcUrl, {
Expand Down Expand Up @@ -412,14 +454,16 @@ export class AssembledTransaction<T> {
}

simulate = async (): Promise<this> => {
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)) {
Expand Down
3 changes: 3 additions & 0 deletions src/contract/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,8 @@ export class Client {
tx,
);
};

txFromXDR = <T>(xdrBase64: string): AssembledTransaction<T> => AssembledTransaction.fromXDR(this.options, xdrBase64, this.spec);

}

13 changes: 7 additions & 6 deletions test/e2e/src/test-swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 840f57d

Please sign in to comment.