-
Notifications
You must be signed in to change notification settings - Fork 30
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
STREAM-1451: solana reliable tx execution #158
Conversation
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
invoker: Keypair | SignerWalletAdapter, | ||
tx: T, | ||
): Promise<T> { | ||
let signedTx: T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let signedTx: T; | |
if (isSignerWallet(invoker)) { | |
return await invoker.signTransaction(tx); | |
} | |
if (isTransactionVersioned(tx)) { | |
return tx.sign([invoker]); | |
} | |
return tx.partialSign(invoker); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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? :)
There was a problem hiding this comment.
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 😔
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 }); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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!, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 || |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this 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