Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
91 changes: 71 additions & 20 deletions account-kit/react/src/hooks/useSolanaTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,33 @@ import { useSyncExternalStore } from "react";
import { useAlchemyAccountContext } from "./useAlchemyAccountContext.js";
import type { PromiseOrValue } from "../../../../aa-sdk/core/dist/types/types.js";

/** Used right before we send the transaction out, this is going to be the signer. */
export type PreSend = (
transaction: VersionedTransaction | Transaction,
next: PreSend
this: void,
transaction: VersionedTransaction | Transaction
) => PromiseOrValue<VersionedTransaction | Transaction>;
/**
* Used in the sendTransaction, will transform either the instructions (or the transfer -> instructions) into a transaction
*/
export type TransformInstruction = (
this: void,
instructions: TransactionInstruction[]
) => PromiseOrValue<Transaction | VersionedTransaction>;
export type SolanaTransactionParamOptions = {
preSend?: PreSend;
transformInstruction?: TransformInstruction;
};
export type SolanaTransactionParams =
| {
transfer: {
amount: number;
toAddress: string;
};
preSend?: PreSend;
transactionComponents?: SolanaTransactionParamOptions;
}
| {
instructions: TransactionInstruction[];
preSend?: PreSend;
transactionComponents?: SolanaTransactionParamOptions;
};
/**
* We wanted to make sure that this will be using the same useMutation that the
Expand All @@ -53,6 +65,10 @@ export interface SolanaTransaction {
readonly isPending: boolean;
/** The error that occurred */
readonly error: Error | null;
/** The signers that can be used to sign the transaction */
signers: Record<string, PreSend>;
/** The map of transform instructions that can be used to transform the instructions */
mapTransformInstructions: Record<string, TransformInstruction>;
reset(): void;
/** Send the transaction */
sendTransaction(params: SolanaTransactionParams): void;
Expand Down Expand Up @@ -106,45 +122,80 @@ export function useSolanaTransaction(
() => getSolanaConnection(config)
);
const mutation = useMutation({
mutationFn: async (params: SolanaTransactionParams) => {
if (!signer) throw new Error("Not ready");
if (!connection) throw new Error("Not ready");
mutationFn: async ({
transactionComponents: {
preSend = signers.fromSigner,
transformInstruction = mapTransformInstructions.default,
} = {},
...params
}: SolanaTransactionParams) => {
const instructions =
"instructions" in params
? params.instructions
: [
SystemProgram.transfer({
fromPubkey: new PublicKey(signer.address),
fromPubkey: new PublicKey(
signer?.address || missing("signer.address")
),
toPubkey: new PublicKey(params.transfer.toAddress),
lamports: params.transfer.amount,
}),
];
const policyId =
"policyId" in opts ? opts.policyId : backupConnection?.policyId;
let transaction: VersionedTransaction | Transaction = policyId
? await signer.addSponsorship(instructions, connection, policyId)
: await signer.createTransfer(instructions, connection);
let transaction: VersionedTransaction | Transaction =
await transformInstruction(instructions);

const iSign: PreSend = async (t) => {
await signer.addSignature(t);
return t;
};
const preSend = params.preSend || iSign;
transaction = await preSend(transaction, iSign);
transaction = await preSend(transaction);

const hash = await solanaNetwork.broadcast(connection, transaction);
const hash = await solanaNetwork.broadcast(
connection || missing("connection"),
transaction
);
return { hash };
},
...opts.mutation,
});
const signers: Record<string, PreSend> = {
async fromSigner(t) {
await signer?.addSignature(t);
return t;
},
};
const signer: null | SolanaSigner = opts?.signer || fallbackSigner;
const connection = opts?.connection || backupConnection?.connection || null;
const policyId =
"policyId" in opts ? opts.policyId : backupConnection?.policyId;
const mapTransformInstructions: Record<string, TransformInstruction> = {
async addSponsorship(instructions: TransactionInstruction[]) {
return await (signer || missing("signer")).addSponsorship(
instructions,
connection || missing("connection"),
policyId || missing("policyId")
);
},
async createTransfer(instructions: TransactionInstruction[]) {
return await (signer || missing("signer")).createTransfer(
instructions,
connection || missing("connection")
);
},
get default() {
return policyId
? mapTransformInstructions.addSponsorship
: mapTransformInstructions.createTransfer;
},
};

return {
connection,
signer,
signers,
mapTransformInstructions,
...mutation,
sendTransaction: mutation.mutate,
sendTransactionAsync: mutation.mutateAsync,
};
}

function missing(message: string): never {
throw new Error(message);
}
17 changes: 8 additions & 9 deletions examples/ui-demo/src/components/small-cards/SolanaNftCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ import { UserAddressTooltip } from "../user-connection-avatar/UserAddressLink";
import { ExternalLinkIcon } from "lucide-react";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
type TransactionState = "idle" | "signing" | "sponsoring" | "complete";
async function PK<T>(t: T) {
return t;
}

const states = [
{
Expand All @@ -57,6 +54,7 @@ export const SolanaNftCard = () => {
isPending,
connection,
signer: solanaSigner,
signers,
} = useSolanaTransaction();
const [transactionState, setTransactionState] =
useState<TransactionState>("idle");
Expand Down Expand Up @@ -101,12 +99,13 @@ export const SolanaNftCard = () => {

const mint = stakeAccount.publicKey;
const tx = await sendTransactionAsync({
preSend: async (transaction, next) => {
if ("version" in transaction) {
transaction.sign([stakeAccount]);
} else {
}
return next(transaction, PK);
transactionComponents: {
preSend: async (transaction) => {
if ("version" in transaction) {
transaction.sign([stakeAccount]);
}
return signers.fromSigner(transaction);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

this is something that app devs need to know about?

is this not something that our hook can do to simplify things?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is in the case we want to over write the sending, for example on this one we have one more signer, and then we need the logic for the previous. It will fail on the simulation for the send transaction if we forgot the original from signer

And in the case of poke the duck, there is no signer except the sponsor, so the overwrite is just an identity function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, the components of the sendSolanaTransaction
(transfer? ) -> instructions --transactionBuilder-> Transaction --preSend-> Broadcast
And now as params are the components that make up this function in the cases we want custom logic, it is easy for a one off change now.

Copy link
Contributor Author

@Blu-J Blu-J May 5, 2025

Choose a reason for hiding this comment

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

stateDiagram-v2
[*] --> instructions
[*] --> transfer
transfer --> instructions
instructions --> Transaction : transformInstruction
Transaction --> broadcastedResult : preSend
Loading

Copy link
Contributor Author

@Blu-J Blu-J May 5, 2025

Choose a reason for hiding this comment

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

Could have also been

 preSend: async (transaction) => {
            if ("version" in transaction) {
              transaction.sign([stakeAccount]);
            }
            await solanaSigner.addSignature(transaction);
            return transaction
          },
     

},
instructions: [
SystemProgram.createAccount({
Expand Down
Loading