Skip to content

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

ts
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:

ts
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.

ts
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

ts
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:

ts
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 prepare to 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 fresh prepare — 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.

SDK pages are generated from the published npm tarballs and cannot drift.