Skip to content
Merged
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
123 changes: 94 additions & 29 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 Down Expand Up @@ -106,39 +118,88 @@ 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");
const instructions =
"instructions" in params
? params.instructions
: [
SystemProgram.transfer({
fromPubkey: new PublicKey(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);
mutationFn: async ({
transactionComponents: {
preSend,
transformInstruction = mapTransformInstructions.default,
} = {},
...params
}: SolanaTransactionParams) => {
const instructions = getInstructions();
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)) || transaction;

const hash = await solanaNetwork.broadcast(connection, transaction);
if (needsSignerToSign()) {
await signer?.addSignature(transaction);
}

const localConnection = connection || missing("connection");
const hash = await solanaNetwork.broadcast(localConnection, transaction);
return { hash };

function getInstructions() {
if ("instructions" in params) {
return params.instructions;
}
return [
SystemProgram.transfer({
fromPubkey: new PublicKey(
signer?.address || missing("signer.address")
),
toPubkey: new PublicKey(params.transfer.toAddress),
lamports: params.transfer.amount,
}),
];
}

function needsSignerToSign() {
if ("message" in transaction) {
const message = transaction.message;
return message.staticAccountKeys.some(
(key, index) =>
(() => {
debugger;
return false;
})() ||
(key.toBase58() === signer?.address &&
message?.isAccountSigner(index))
);
}
return transaction.instructions.some((x) =>
x.keys.some(
(x) => x.pubkey.toBase58() === signer?.address && x.isSigner
)
);
}
},
...opts.mutation,
});
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,
Expand All @@ -148,3 +209,7 @@ export function useSolanaTransaction(
sendTransactionAsync: mutation.mutateAsync,
};
}

function missing(message: string): never {
throw new Error(message);
}
22 changes: 12 additions & 10 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 Down Expand Up @@ -72,7 +69,6 @@ export const SolanaNftCard = () => {
);
},
});

const handleCollectNFT = async () => {
try {
if (!solanaSigner) throw new Error("No signer found");
Expand Down Expand Up @@ -101,12 +97,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 transaction;
},
},
instructions: [
SystemProgram.createAccount({
Expand Down Expand Up @@ -156,6 +153,11 @@ export const SolanaNftCard = () => {
https://explorer.solana.com/address/${mint.toBase58()}?cluster=devnet
`);

setToast({
type: "success",
text: "Transaction sent",
open: true,
});
setTransactionState("complete");
} catch (error) {
console.log(error);
Expand Down
Loading