Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
Expand Down
7 changes: 5 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"devDependencies": {
"@types/node": "20.4.2",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"@types/promise-retry": "^1.1.3"
},
"keywords": [],
"author": "",
Expand All @@ -20,6 +21,8 @@
"@jup-ag/referral-sdk": "0.1.6",
"@solana/spl-token": "0.3.8",
"@solana/web3.js": "^1.77.3",
"dotenv": "^16.3.1"
"dotenv": "^16.3.1",
"promise-retry": "2.0.1",
"bs58": "^5.0.0"
}
}
55 changes: 39 additions & 16 deletions example/src/claimAll.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { ReferralProvider } from "@jup-ag/referral-sdk";
import {
Connection,
Keypair,
PublicKey,
sendAndConfirmRawTransaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";

import { getSignature } from "./utils/getSignature";
import { transactionSenderAndConfirmationWaiter } from "./utils/transactionSender";

const connection = new Connection(process.env.RPC_URL || "");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.KEYPAIR || ""));
Expand All @@ -27,18 +24,44 @@ const provider = new ReferralProvider(connection);
// Send each claim transaction one by one.
for (const tx of txs) {
tx.sign([keypair]);
const signature = getSignature(tx);

// We first simulate whether the transaction would be successful
const { value: simulatedTransactionResponse } =
await connection.simulateTransaction(tx, {
replaceRecentBlockhash: true,
commitment: "processed",
});
const { err, logs } = simulatedTransactionResponse;

if (err) {
// Simulation error, we can check the logs for more details
// If you are getting an invalid account error, make sure that you have the input mint account to actually swap from.
console.error("Simulation Error:");
console.error({ err, logs });
continue;
}

const txid = await connection.sendTransaction(tx);
const { value } = await connection.confirmTransaction({
signature: txid,
blockhash,
lastValidBlockHeight,
const serializedTransaction = Buffer.from(tx.serialize());
const transactionResponse = await transactionSenderAndConfirmationWaiter({
connection,
serializedTransaction,
blockhashWithExpiryBlockHeight: {
blockhash,
lastValidBlockHeight: lastValidBlockHeight,
},
});

if (value.err) {
console.log({ value, txid });
} else {
console.log({ txid });
// If we are not getting a response back, the transaction has not confirmed.
if (!transactionResponse) {
console.error("Transaction not confirmed");
continue;
}

if (transactionResponse.meta?.err) {
console.error(transactionResponse.meta?.err);
}

console.log(`https://solscan.io/tx/${signature}`);
}
})();
17 changes: 17 additions & 0 deletions example/src/utils/getSignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Transaction, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

export function getSignature(
transaction: Transaction | VersionedTransaction,
): string {
const signature =
"signature" in transaction
? transaction.signature
: transaction.signatures[0];
if (!signature) {
throw new Error(
"Missing transaction signature, the transaction was not signed by the fee payer",
);
}
return bs58.encode(signature);
}
109 changes: 109 additions & 0 deletions example/src/utils/transactionSender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
BlockhashWithExpiryBlockHeight,
Connection,
TransactionExpiredBlockheightExceededError,
VersionedTransactionResponse,
} from "@solana/web3.js";
import promiseRetry from "promise-retry";

import { wait } from "./wait";

type TransactionSenderAndConfirmationWaiterArgs = {
connection: Connection;
serializedTransaction: Buffer;
blockhashWithExpiryBlockHeight: BlockhashWithExpiryBlockHeight;
};

const SEND_OPTIONS = {
skipPreflight: true,
};

export async function transactionSenderAndConfirmationWaiter({
connection,
serializedTransaction,
blockhashWithExpiryBlockHeight,
}: TransactionSenderAndConfirmationWaiterArgs): Promise<VersionedTransactionResponse | null> {
const txid = await connection.sendRawTransaction(
serializedTransaction,
SEND_OPTIONS,
);

const controller = new AbortController();
const abortSignal = controller.signal;

const abortableResender = async () => {
while (true) {
await wait(2_000);
if (abortSignal.aborted) return;
try {
await connection.sendRawTransaction(
serializedTransaction,
SEND_OPTIONS,
);
} catch (e) {
console.warn(`Failed to resend transaction: ${e}`);
}
}
};

try {
abortableResender();
const lastValidBlockHeight =
blockhashWithExpiryBlockHeight.lastValidBlockHeight - 150;

// this would throw TransactionExpiredBlockheightExceededError
await Promise.race([
connection.confirmTransaction(
{
...blockhashWithExpiryBlockHeight,
lastValidBlockHeight,
signature: txid,
abortSignal,
},
"confirmed",
),
new Promise(async (resolve) => {
// in case ws socket died
while (!abortSignal.aborted) {
await wait(2_000);
const tx = await connection.getSignatureStatus(txid, {
searchTransactionHistory: false,
});
if (tx?.value?.confirmationStatus === "confirmed") {
resolve(tx);
}
}
}),
]);
} catch (e) {
if (e instanceof TransactionExpiredBlockheightExceededError) {
// we consume this error and getTransaction would return null
return null;
} else {
// invalid state from web3.js
throw e;
}
} finally {
controller.abort();
}

// in case rpc is not synced yet, we add some retries
const response = promiseRetry(
async (retry) => {
const response = await connection.getTransaction(txid, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
if (!response) {
retry(response);
}
return response;
},
{
retries: 5,
minTimeout: 1e3,
},
);

return response;
}
2 changes: 2 additions & 0 deletions example/src/utils/wait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const wait = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time));
30 changes: 30 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/promise-retry@^1.1.3":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@types/promise-retry/-/promise-retry-1.1.6.tgz#3c48826d8a27f68f9d4900fc7448f08a1532db44"
integrity sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA==
dependencies:
"@types/retry" "*"

"@types/retry@*":
version "0.12.5"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e"
integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==

"@types/ws@^7.4.4":
version "7.4.7"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
Expand Down Expand Up @@ -356,6 +368,11 @@ dotenv@^16.3.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==

err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==

es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
Expand Down Expand Up @@ -485,11 +502,24 @@ pako@^2.0.3:
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==

promise-retry@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
dependencies:
err-code "^2.0.2"
retry "^0.12.0"

regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==

retry@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==

rpc-websockets@^7.5.1:
version "7.9.0"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.9.0.tgz#a3938e16d6f134a3999fdfac422a503731bf8973"
Expand Down