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

STREAM-1451: solana reliable tx execution #158

Merged
merged 7 commits into from
Apr 5, 2024

Conversation

Yolley
Copy link
Collaborator

@Yolley Yolley commented Apr 5, 2024

  • simulate and send transaction separately
  • rebroadcast transaction until we're sure it hasn't or has landed
  • switch to VersionedTransaction

@Yolley Yolley requested review from RolginRoman and LukaStreamflow and removed request for RolginRoman April 5, 2024 07:09
invoker: Keypair | SignerWalletAdapter,
tx: T,
): Promise<T> {
let signedTx: T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let signedTx: T;
if (isSignerWallet(invoker)) {
return await invoker.signTransaction(tx);
}
if (isTransactionVersioned(tx)) {
return tx.sign([invoker]);
}
return tx.partialSign(invoker);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it more readable

for (let i = 0; i < 3; i++) {
let res: RpcResponseAndContext<SimulatedTransactionResponse>;
if (isTransactionVersioned(tx)) {
res = await connection.simulateTransaction(tx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be the same as else? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought, but looks like method overloading in TS works differently and it's incapable of handling this correctly. Therefore, this will produce an error because of polymorphic input. I guess there is some way of casting tx correctly, but it won't make this place easier to read 😔

Copy link
Contributor

@RolginRoman RolginRoman Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's more likely btw, solana.web3 produces incomplete type declaration for this function

res = await connection.simulateTransaction(tx);
}
if (res.value.err) {
const errMessage = JSON.stringify(res.value.err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Let's be fancy and specify number of retries as constant, instead of "3".
Will be more readable also

throw Error("Error with transaction parameters.");
}

for (let i = 0; i < 3; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put whole simulation stuff into function

let blockheight = await connection.getBlockHeight(commitment);
let transactionSent = false;
const rawTransaction = tx.serialize();
while (blockheight < hash.lastValidBlockHeight) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose extracting this in a function also

}

/**
* Confirms and validates transaction success once
* @param connection - Solana client connection
* @param signature - Transaction signature
* @param passError - return status even if tx failed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find naming a bit strange, what should passError mean? Could be just successful?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe better, ignoreError, if we are just printing it ant not throwing

);
return signAndExecuteTransaction(connection, invoker, tx, hash);
return signAndExecuteTransaction(connection, invoker, tx, { hash, context, commitment });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely split utils into meaningful files :) I guess for one of next PRs

@@ -230,8 +238,12 @@ export default class SolanaDistributorClient {

ixs.push(clawback(accounts, this.programId));

const { tx, hash } = await prepareTransaction(this.connection, ixs, invoker.publicKey);
const signature = await wrappedSignAndExecuteTransaction(this.connection, invoker, tx, hash);
const { tx, hash, context } = await prepareTransaction(this.connection, ixs, invoker.publicKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 times copy pasta

@@ -799,8 +829,12 @@ export default class SolanaStreamClient extends BaseStreamClient {
*/
public async update(data: IUpdateData, extParams: IInteractStreamSolanaExt): Promise<ITransactionResult> {
const ixs = await this.prepareUpdateInstructions(data, extParams);
const { tx, hash } = await prepareTransaction(this.connection, ixs, extParams.invoker.publicKey);
const signature = await signAndExecuteTransaction(this.connection, extParams.invoker, tx, hash);
const { tx, hash, context } = await prepareTransaction(this.connection, ixs, extParams.invoker.publicKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 times copy pasta

@@ -866,7 +900,8 @@ export default class SolanaStreamClient extends BaseStreamClient {
}

public extractErrorCode(err: Error): string | null {
return extractSolanaErrorCode(err.toString() ?? "Unknown error!");
const logs = "logs" in err && Array.isArray(err.logs) ? err.logs : undefined;
return extractSolanaErrorCode(err.toString() ?? "Unknown error!", logs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

* @returns Transaction Status
*/
export async function confirmAndEnsureTransaction(
connection: Connection,
signature: string,
passError?: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer more direct naming as throwError as it points to clearer understanding of its usage

imo passError might mean that either error is going to be passed as value or error is going go to be skipped (as pass ~= skip in this case)

}
const { value: hash, context } = await connection.getLatestBlockhashAndContext(commitment);
const messageV0 = new TransactionMessage({
payerKey: payer!,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im wondering if it is a valid case if the payer is falsy.

should we fail early in this case? should we disallow this case on type layer?

throw new SendTransactionError("failed to simulate transaction: " + errMessage, res.value.logs || undefined);
}
}
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it unused? it is wrapping instruction already

throw Error("Error with transaction parameters.");
}

for (let i = 0; i < 3; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read the js doc here :d
but why 3?

I'd say it should be a time-constrained retry ?

}
throw e;
}
await sleep(500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why should it sleep every time ? May it skip sleep if transactionSent === true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We rebroadcasting here and sending transaction over and over again

transactionSent = true;
} catch (e) {
if (
transactionSent ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if transactionSent == true, why we continue aka execute another loop cycle aka sendRawTransaction again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are rebroadcasting, please read the comments of the function

Copy link
Contributor

@RolginRoman RolginRoman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could convert comments into follow-up issues/tasks for implementing. But since it is urgent (as always), we can try this V in the app

@RolginRoman RolginRoman merged commit 170947f into master Apr 5, 2024
2 checks passed
Copy link

github-actions bot commented Apr 5, 2024

@RolginRoman RolginRoman deleted the oleg/feature/STREAM-1451_reliable_tx_execution branch April 5, 2024 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants