Wallets and signing
GraveMint ships no chain SDK. You bring whatever wallet library you already use, and hand us one function. That is the whole integration surface, and it is deliberately this small so the SDK cannot collide with your stack.
The one rule
You sign. We broadcast.
Never submit a mint transaction yourself. Sign the bytes prepare returned and send them back; we submit and confirm.
A transaction we never inspected is one whose recipient, amount and instructions we never verified. The server does a full forward pass over the signed bytes — program ids, instruction data and resolved account pubkeys — and rejects anything added or altered with TX_MODIFIED.
Sending a transactionHash instead is refused with CLIENT_BROADCAST_NOT_ALLOWED, and both codes are terminal: retrying the same body cannot succeed.
The interface
interface Signer {
/** base64 in, base64 out. */
signTransaction(transactionBase64: string): Promise<string>;
/** Optional. Used when quantity > 1. */
signAllTransactions?(transactionsBase64: string[]): Promise<string[]>;
}That is it. No connection object, no RPC endpoint, no keypair.
Solana wallet-adapter
The common case. @solana/wallet-adapter-react gives you a VersionedTransaction signer; GraveMint speaks base64, so you deserialize and re-serialize:
import { useWallet } from '@solana/wallet-adapter-react';
import { VersionedTransaction } from '@solana/web3.js';
const { signTransaction, signAllTransactions } = useWallet();
const signer = {
async signTransaction(b64: string) {
const tx = VersionedTransaction.deserialize(Buffer.from(b64, 'base64'));
const signed = await signTransaction!(tx);
return Buffer.from(signed.serialize()).toString('base64');
},
async signAllTransactions(list: string[]) {
const txs = list.map((b) => VersionedTransaction.deserialize(Buffer.from(b, 'base64')));
const signed = await signAllTransactions!(txs);
return signed.map((t) => Buffer.from(t.serialize()).toString('base64'));
},
};Handle both transaction shapes
Older drops may return a legacy Transaction rather than a VersionedTransaction. If VersionedTransaction.deserialize throws, fall back to Transaction.from(buffer) and serialize with { requireAllSignatures: false }.
Privy and embedded wallets
Privy exposes a Solana signer through its wallet object. The adapter is the same shape — deserialize, sign, re-serialize. The only difference is where the signer comes from.
Quantity above 1 is a BATCH
Every Solana NFT standard mints one NFT per transaction, so any quantity above 1 returns several transactions and several sessions.
This changes where errors appear
For a single mint the server promotes the per-transaction code to the top level, so err.code === 'TX_MODIFIED' works. For a batch the codes stay inside results[].errorCode and err.code is never set.
const code = err.code ?? err.results?.find((r) => r.errorCode)?.errorCode;Handle both, or a batch failure looks like no failure at all.
The full flow
const prep = await gm.mint.prepare({ collectionId, phaseId, walletAddress, quantity: 1 });
const signed = await signer.signTransaction(prep.transactions[0]);
const result = await gm.mint.execute({
sessionId: prep.sessions[0],
signedTransaction: signed, // never a transactionHash
});Or let the SDK drive all three:
await gm.mint.mint({ collectionId, phaseId, walletAddress, quantity: 1, signer });Sessions expire
prepare reserves: it takes a three-minute NFT lock and may reserve against the BOGO and claim-code ledgers. So:
- Do not call
prepareto display a price. Quoting on page load would lock supply for every visitor. Prices come from the read model, already resolved. - A signature held too long fails with
SESSION_EXPIRED. Start over with a freshprepare— do not retry the old session. - Replaying a session loses to an atomic claim and returns
409.
Chains
v1 is Solana-only. A drop on another chain returns CHAIN_NOT_SUPPORTED_BY_SURFACE, which is distinct from CHAIN_DISABLED (a chain retired platform-wide). Use capabilities.mintUrl to send the collector to GraveMint instead.
This is not an arbitrary limit: Solana is where the server-side transaction pin is genuinely strong, and it covers 81% of live collections.
