Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: separate sign and send #987

Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ A breaking change will get clearly marked in this log.

## Unreleased

### Added
- `contract.AssembledTransaction` now supports separate `sign` and `send` methods so that you can sign a transaction without sending it.


## [v12.0.1](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.1)

Expand Down
72 changes: 66 additions & 6 deletions src/contract/assembled_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ export class AssembledTransaction<T> {
*/
private server: Server;

/**
* The signed transaction.
*/
private signed?: Tx;

/**
* A list of the most important errors that various AssembledTransaction
* methods can throw. Feel free to catch specific errors in your application
Expand Down Expand Up @@ -506,11 +511,9 @@ export class AssembledTransaction<T> {
/**
* Sign the transaction with the `wallet`, included previously. If you did
* not previously include one, you need to include one now that at least
BlaineHeffron marked this conversation as resolved.
Show resolved Hide resolved
* includes the `signTransaction` method. After signing, this method will
* send the transaction to the network and return a `SentTransaction` that
* keeps track of all the attempts to fetch the transaction.
* includes the `signTransaction` method.
*/
signAndSend = async ({
sign = async ({
force = false,
signTransaction = this.options.signTransaction,
}: {
Expand All @@ -522,7 +525,7 @@ export class AssembledTransaction<T> {
* You must provide this here if you did not provide one before
*/
signTransaction?: ClientOptions["signTransaction"];
} = {}): Promise<SentTransaction<T>> => {
} = {}): Promise<void> => {
if (!this.built) {
throw new Error("Transaction has not yet been simulated");
}
Expand All @@ -548,9 +551,66 @@ export class AssembledTransaction<T> {
);
}

const timeoutInSeconds =
this.options.timeoutInSeconds ?? DEFAULT_TIMEOUT;
this.built = TransactionBuilder.cloneFrom(this.built!, {
fee: this.built!.fee,
timebounds: undefined,
sorobanData: this.simulationData.transactionData,
})
.setTimeout(timeoutInSeconds)
.build();

const signature = await signTransaction(
this.built.toXDR(),
{
networkPassphrase: this.options.networkPassphrase,
},
);

this.signed = TransactionBuilder.fromXDR(
signature,
this.options.networkPassphrase,
) as Tx;
};

/**
* Sends the transaction to the network to return a `SentTransaction` that
* keeps track of all the attempts to fetch the transaction.
*/
async send(){
if(!this.signed){
throw new Error("The transaction has not yet been signed. Run `sign` first.");
BlaineHeffron marked this conversation as resolved.
Show resolved Hide resolved
}
const typeChecked: AssembledTransaction<T> = this;
const sent = await SentTransaction.init(signTransaction, typeChecked);
const sent = await SentTransaction.init(typeChecked, this.signed);
return sent;
}

/**
* Sign the transaction with the `wallet`, included previously. If you did
* not previously include one, you need to include one now that at least
* includes the `signTransaction` method. After signing, this method will
BlaineHeffron marked this conversation as resolved.
Show resolved Hide resolved
* send the transaction to the network and return a `SentTransaction` that
* keeps track of all the attempts to fetch the transaction.
*/
signAndSend = async ({
force = false,
signTransaction = this.options.signTransaction,
}: {
/**
* If `true`, sign and send the transaction even if it is a read call
*/
force?: boolean;
/**
* You must provide this here if you did not provide one before
*/
signTransaction?: ClientOptions["signTransaction"];
} = {}): Promise<SentTransaction<T>> => {
if(!this.signed){
await this.sign({ force, signTransaction });
}
return this.send();
};

private getStorageExpiration = async () => {
Expand Down
48 changes: 9 additions & 39 deletions src/contract/sent_transaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* disable max-classes rule, because extending error shouldn't count! */
/* eslint max-classes-per-file: 0 */
import { SorobanDataBuilder, TransactionBuilder } from "@stellar/stellar-base";
import type { ClientOptions, MethodOptions, Tx } from "./types";
import type { MethodOptions, Tx } from "./types";
import { Server } from "../rpc/server"
import { Api } from "../rpc/api"
import { DEFAULT_TIMEOUT, withExponentialBackoff } from "./utils";
Expand All @@ -24,8 +23,6 @@ import type { AssembledTransaction } from "./assembled_transaction";
export class SentTransaction<T> {
public server: Server;

public signed?: Tx;

/**
* The result of calling `sendTransaction` to broadcast the transaction to the
* network.
Expand Down Expand Up @@ -53,61 +50,32 @@ export class SentTransaction<T> {
};

constructor(
public signTransaction: ClientOptions["signTransaction"],
public assembled: AssembledTransaction<T>,

public signed: Tx,
) {
if (!signTransaction) {
throw new Error(
"You must provide a `signTransaction` function to send a transaction",
);
}
this.server = new Server(this.assembled.options.rpcUrl, {
allowHttp: this.assembled.options.allowHttp ?? false,
});
}

/**
* Initialize a `SentTransaction` from an existing `AssembledTransaction` and
* a `signTransaction` function. This will also send the transaction to the
* a `signed` AssembledTransaction. This will also send the transaction to the
BlaineHeffron marked this conversation as resolved.
Show resolved Hide resolved
* network.
*/
static init = async <U>(
/** More info in {@link MethodOptions} */
signTransaction: ClientOptions["signTransaction"],
/** {@link AssembledTransaction} from which this SentTransaction was initialized */
assembled: AssembledTransaction<U>,
/** The signed transaction to send to the network */
signed: Tx,
): Promise<SentTransaction<U>> => {
const tx = new SentTransaction(signTransaction, assembled);
const tx = new SentTransaction(assembled, signed);
const sent = await tx.send();
return sent;
};

private send = async (): Promise<this> => {
const timeoutInSeconds =
this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT;
this.assembled.built = TransactionBuilder.cloneFrom(this.assembled.built!, {
fee: this.assembled.built!.fee,
timebounds: undefined,
sorobanData: new SorobanDataBuilder(
this.assembled.simulationData.transactionData.toXDR(),
).build(),
})
.setTimeout(timeoutInSeconds)
.build();

const signature = await this.signTransaction!(
// `signAndSend` checks for `this.built` before calling `SentTransaction.init`
this.assembled.built!.toXDR(),
{
networkPassphrase: this.assembled.options.networkPassphrase,
},
);

this.signed = TransactionBuilder.fromXDR(
signature,
this.assembled.options.networkPassphrase,
) as Tx;

this.sendTransactionResponse = await this.server.sendTransaction(
this.signed,
);
Expand All @@ -124,6 +92,8 @@ export class SentTransaction<T> {

const { hash } = this.sendTransactionResponse;

const timeoutInSeconds =
this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT;
this.getTransactionResponseAll = await withExponentialBackoff(
() => this.server.getTransaction(hash),
(resp) => resp.status === Api.GetTransactionStatus.NOT_FOUND,
Expand Down
Loading