Skip to content

Commit aa28323

Browse files
committed
Refactor transaction sending and package structure
1 parent 9ca6d30 commit aa28323

File tree

15 files changed

+376
-284
lines changed

15 files changed

+376
-284
lines changed

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ bun run ci:publish # Publish packages to npm
4444
cd apps/example-dapp
4545
bun run dev # Start Vite dev server
4646
bun run build # Build the app
47+
48+
# IMPORTANT: After making code changes
49+
bun run build # Build to check for TypeScript errors
50+
bun run lint:fix # Fix linting and formatting issues
4751
```
4852

4953
## Architecture
@@ -106,6 +110,11 @@ Provides two contexts:
106110
- **Use double quotes for strings** (not single quotes)
107111
- Follow default Prettier settings
108112

113+
### After Making Code Changes
114+
**Always run these commands to ensure code quality:**
115+
1. `bun run build` - Check for TypeScript errors
116+
2. `bun run lint:fix` - Fix linting and formatting issues
117+
109118
### React Components
110119
- Small, focused components
111120
- Use function components with hooks

apps/example-dapp/src/routes/examples/wrapped-sol.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const WrappedSOLPage: FC = () => {
107107
setWrapAmount("");
108108
} catch (error) {
109109
console.error("Error wrapping SOL:", error);
110-
// Error already handled by sendTX toast
110+
// Error already handled by mutation
111111
} finally {
112112
setIsWrapping(false);
113113
}
@@ -135,7 +135,7 @@ const WrappedSOLPage: FC = () => {
135135
console.log("Transaction:", explorerLink);
136136
} catch (error) {
137137
console.error("Error closing wSOL account:", error);
138-
// Error already handled by sendTX toast
138+
// Error already handled by mutation
139139
} finally {
140140
setIsClosing(false);
141141
}

devenv.nix

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
packages = with pkgs; [
66
git
77
nixfmt-rfc-style
8-
biome
98
];
109

1110
languages.javascript = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"changeset:version": "changeset version",
3737
"changeset:publish": "changeset publish",
3838
"ci:version": "changeset version && bun update",
39-
"ci:publish": "changeset publish",
39+
"ci:publish": "for dir in packages/*; do (cd \"$dir\" && bun publish || true); done && changeset tag",
4040
"prepare": "husky",
4141
"codegen": "turbo run codegen"
4242
},

packages/grill/src/contexts/grill-context.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DataLoader } from "@macalinao/dataloader-es";
22
import type { Address, EncodedAccount } from "@solana/kit";
33
import { createContext, useContext } from "react";
4-
import type { TransactionStatusEventCallback } from "../types.js";
4+
import type { SendTXFunction } from "../utils/internal/create-send-tx.js";
55

66
/**
77
* Context value interface for SolanaAccountProvider.
@@ -10,12 +10,12 @@ import type { TransactionStatusEventCallback } from "../types.js";
1010
export interface GrillContextValue {
1111
/** DataLoader instance for batching and caching Solana account requests */
1212
accountLoader: DataLoader<Address, EncodedAccount | null>;
13-
reloadAccounts: (addresses: Address[]) => Promise<void>;
13+
refetchAccounts: (addresses: Address[]) => Promise<void>;
1414

1515
/**
16-
* Internal callback for sending transaction status events.
16+
* Function to send transactions with batching and confirmation
1717
*/
18-
internal_onTransactionStatusEvent: TransactionStatusEventCallback;
18+
sendTX: SendTXFunction;
1919
}
2020

2121
/**
Lines changed: 7 additions & 267 deletions
Original file line numberDiff line numberDiff line change
@@ -1,275 +1,15 @@
1-
import type {
2-
AddressesByLookupTableAddress,
3-
Instruction,
4-
Signature,
5-
TransactionSigner,
6-
} from "@solana/kit";
7-
import {
8-
compressTransactionMessageUsingAddressLookupTables,
9-
getBase58Decoder,
10-
signAndSendTransactionMessageWithSigners,
11-
} from "@solana/kit";
12-
import { createTransaction, getExplorerLink } from "gill";
13-
14-
import { useSolanaClient } from "gill-react";
15-
import { useCallback } from "react";
161
import { useGrillContext } from "../contexts/grill-context.js";
17-
import { useKitWallet } from "./use-kit-wallet.js";
18-
19-
export type TransactionId = string;
20-
21-
export type TransactionStatusEvent = {
22-
title: string;
23-
id: TransactionId;
24-
} & (
25-
| {
26-
type: "error-wallet-not-connected";
27-
}
28-
| {
29-
type: "preparing";
30-
}
31-
| {
32-
type: "awaiting-wallet-signature";
33-
}
34-
| {
35-
type: "waiting-for-confirmation";
36-
sig: Signature;
37-
explorerLink: string;
38-
}
39-
| {
40-
type: "confirmed";
41-
sig: Signature;
42-
explorerLink: string;
43-
}
44-
| {
45-
type: "error-transaction-failed";
46-
errorMessage: string;
47-
sig: Signature;
48-
explorerLink: string;
49-
}
50-
);
51-
52-
export interface SendTXOptions {
53-
luts?: AddressesByLookupTableAddress;
54-
signers?: TransactionSigner[];
55-
}
562

57-
export type SendTXFunction = (
58-
name: string,
59-
ixs: readonly Instruction[],
60-
options?: SendTXOptions,
61-
) => Promise<Signature>;
3+
export type {
4+
SendTXFunction,
5+
SendTXOptions,
6+
} from "../utils/internal/create-send-tx.js";
627

638
/**
649
* Hook that provides a function to send transactions using the modern @solana/kit API
6510
* while maintaining compatibility with the wallet adapter.
6611
*/
67-
export const useSendTX = (): SendTXFunction => {
68-
const { reloadAccounts, internal_onTransactionStatusEvent } =
69-
useGrillContext();
70-
const { signer } = useKitWallet();
71-
const { rpc } = useSolanaClient();
72-
return useCallback(
73-
async (
74-
name: string,
75-
ixs: readonly Instruction[],
76-
options: SendTXOptions = {},
77-
): Promise<Signature> => {
78-
const txId = Math.random().toString(36).substring(2, 15);
79-
const baseEvent = {
80-
id: txId,
81-
title: name,
82-
};
83-
if (!signer) {
84-
internal_onTransactionStatusEvent({
85-
...baseEvent,
86-
type: "error-wallet-not-connected",
87-
});
88-
throw new Error("Wallet not connected");
89-
}
90-
91-
internal_onTransactionStatusEvent({
92-
...baseEvent,
93-
type: "preparing",
94-
});
95-
96-
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
97-
const transactionMessage = createTransaction({
98-
version: 0,
99-
feePayer: signer,
100-
instructions: [...ixs],
101-
latestBlockhash,
102-
// the compute budget values are HIGHLY recommend to be set in order to maximize your transaction landing rate
103-
// TODO(igm): make this configurable and/or dynamic based on the instructions
104-
computeUnitLimit: 1_400_000,
105-
computeUnitPrice: 100_000n,
106-
});
107-
108-
// Apply address lookup tables if provided to compress the transaction
109-
const addressLookupTables = options.luts ?? {};
110-
const finalTransactionMessage =
111-
Object.keys(addressLookupTables).length > 0
112-
? compressTransactionMessageUsingAddressLookupTables(
113-
transactionMessage,
114-
addressLookupTables,
115-
)
116-
: transactionMessage;
117-
118-
internal_onTransactionStatusEvent({
119-
...baseEvent,
120-
type: "awaiting-wallet-signature",
121-
});
122-
123-
// Send transaction using wallet adapter
124-
const sigBytes = await signAndSendTransactionMessageWithSigners(
125-
finalTransactionMessage,
126-
);
127-
const decoder = getBase58Decoder();
128-
const sig = decoder.decode(sigBytes) as Signature;
129-
const sentTxEvent = {
130-
...baseEvent,
131-
sig,
132-
explorerLink: getExplorerLink({ transaction: sig }),
133-
};
134-
135-
internal_onTransactionStatusEvent({
136-
...sentTxEvent,
137-
type: "waiting-for-confirmation",
138-
});
139-
140-
try {
141-
// Wait for confirmation using modern RPC
142-
const confirmationStrategy = {
143-
signature: sig,
144-
blockhash: latestBlockhash.blockhash,
145-
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
146-
};
147-
148-
// Poll for transaction confirmation
149-
let confirmed = false;
150-
let confirmationError: Error | null = null;
151-
const maxRetries = 30;
152-
let retries = 0;
153-
154-
while (retries < maxRetries) {
155-
try {
156-
const signatureStatus = await rpc
157-
.getSignatureStatuses([sig])
158-
.send();
159-
160-
if (signatureStatus.value[0]) {
161-
const status = signatureStatus.value[0];
162-
if (
163-
status.confirmationStatus === "confirmed" ||
164-
status.confirmationStatus === "finalized"
165-
) {
166-
confirmed = true;
167-
if (status.err) {
168-
confirmationError = new Error("Transaction failed on-chain");
169-
}
170-
break;
171-
}
172-
}
173-
174-
// Check if blockhash is still valid
175-
const blockHeight = await rpc.getBlockHeight().send();
176-
if (blockHeight > confirmationStrategy.lastValidBlockHeight) {
177-
throw new Error(
178-
"Transaction expired - blockhash no longer valid",
179-
);
180-
}
181-
182-
// Wait before next attempt
183-
await new Promise((resolve) => setTimeout(resolve, 1000));
184-
retries++;
185-
} catch (error) {
186-
console.error("Error checking transaction status:", error);
187-
throw error;
188-
}
189-
}
190-
191-
if (!confirmed) {
192-
throw new Error("Transaction confirmation timeout");
193-
}
194-
195-
if (confirmationError) {
196-
throw confirmationError;
197-
}
198-
199-
// Get transaction details for logging using modern RPC
200-
const result = await rpc
201-
.getTransaction(sig, {
202-
commitment: "confirmed",
203-
maxSupportedTransactionVersion: 0,
204-
encoding: "jsonParsed",
205-
})
206-
.send();
207-
208-
if (result) {
209-
// Reload the accounts that were written to
210-
const writableAccounts = result.transaction.message.accountKeys
211-
.filter((key) => key.writable)
212-
.map((k) => k.pubkey);
213-
await reloadAccounts(writableAccounts);
214-
}
215-
216-
internal_onTransactionStatusEvent({
217-
...sentTxEvent,
218-
type: "confirmed",
219-
});
220-
221-
if (result?.meta?.logMessages) {
222-
console.log(name, result.meta.logMessages.join("\n"));
223-
}
224-
225-
// Return the signature as a base58 string
226-
return sig;
227-
} catch (error: unknown) {
228-
// Log error details for debugging
229-
console.error(`${name} transaction failed:`, error);
230-
231-
// Extract error logs
232-
const extractErrorLogs = (err: unknown): string[] => {
233-
if (
234-
err &&
235-
typeof err === "object" &&
236-
"logs" in err &&
237-
Array.isArray((err as { logs: unknown }).logs)
238-
) {
239-
return (err as { logs: string[] }).logs;
240-
}
241-
if (
242-
err &&
243-
typeof err === "object" &&
244-
"context" in err &&
245-
typeof (err as { context: unknown }).context === "object" &&
246-
(err as { context: { logs?: unknown } }).context.logs &&
247-
Array.isArray((err as { context: { logs: unknown } }).context.logs)
248-
) {
249-
return (err as { context: { logs: string[] } }).context.logs;
250-
}
251-
return [];
252-
};
253-
254-
const errorLogs = extractErrorLogs(error);
255-
if (errorLogs.length > 0) {
256-
console.log("Transaction logs:");
257-
for (const log of errorLogs) {
258-
console.log(" ", log);
259-
}
260-
}
261-
262-
const errorMessage =
263-
error instanceof Error ? error.message : "Transaction failed.";
264-
265-
internal_onTransactionStatusEvent({
266-
...sentTxEvent,
267-
type: "error-transaction-failed",
268-
errorMessage,
269-
});
270-
throw error;
271-
}
272-
},
273-
[internal_onTransactionStatusEvent, reloadAccounts, rpc, signer],
274-
);
12+
export const useSendTX = () => {
13+
const { sendTX } = useGrillContext();
14+
return sendTX;
27515
};

packages/grill/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./contexts/index.js";
22
export * from "./hooks/index.js";
33
export * from "./providers/index.js";
44
export * from "./types.js";
5+
export * from "./utils/index.js";

0 commit comments

Comments
 (0)