Issuer Integration
Issuer setup script
This page shows the minimal script flow to:
- deploy a verifier program
- register that verifier in SSTS verification config
- issue a token with that verification config
Reference implementation source: ssts-example/scripts/issuer-setup.ts.
Flow
Rendering diagram...
1) Deploy verifier program
CLUSTER=devnet npm run build
CLUSTER=devnet npm run deploy:verification
Then put the deployed verifier ID in config/program-ids.json for your cluster.
2) Minimal program-id config
Use the core ID registry in this docs repo as the source of truth:
{
"localnet": {
"securityTokenProgram": null,
"transferHookProgram": null
},
"devnet": {
"securityTokenProgram": "SSTS8Qk2bW3aVaBEsY1Ras95YdbaaYQQx21JWHxvjap",
"transferHookProgram": "HookXqLKgPaNrHBJ9Jui7oQZz93vMbtA88JjsLa8bmfL"
},
"testnet": {
"securityTokenProgram": null,
"transferHookProgram": null
},
"mainnet": {
"securityTokenProgram": null,
"transferHookProgram": null
}
}In the issuer repo (ssts-example), set config/program-ids.json by adding your verifier program ID for the same cluster:
{
"devnet": {
"securityTokenProgram": "<core security token program id>",
"transferHookProgram": "<core transfer hook program id>",
"transferWhitelistProgram": "<your deployed verifier program id>"
}
}
3) Minimal TypeScript setup flow
This is a trimmed example to show required instruction sequence. For exact module-loading helpers, mirror scripts/issuer-setup.ts.
import {
AccountRole,
address,
appendTransactionMessageInstructions,
createSolanaRpc,
createTransactionMessage,
sendTransactionWithoutConfirmingFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit"
import fs from "node:fs"
import path from "node:path"
import {
getInitializeMintInstruction,
} from "../clients/typescript/src/generated/instructions/initializeMint"
import {
getInitializeVerificationConfigInstruction,
} from "../clients/typescript/src/generated/instructions/initializeVerificationConfig"
import {
getMintInstruction,
} from "../clients/typescript/src/generated/instructions/mint"
const INSTRUCTION_DISCRIMINATORS = { mint: 6, transfer: 12 } as const
const INSTRUCTIONS_SYSVAR = address("Sysvar1nstructions1111111111111111111111111")
const TOKEN_2022_PROGRAM = address("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
type Cluster = "localnet" | "devnet" | "testnet" | "mainnet"
type ProgramIds = {
securityTokenProgram: string | null
transferHookProgram: string | null
transferWhitelistProgram: string | null
}
const cluster = (process.env.CLUSTER ?? "devnet") as Cluster
const configPath = path.resolve("config/program-ids.json")
const allProgramIds = JSON.parse(
fs.readFileSync(configPath, "utf8"),
) as Record<Cluster, ProgramIds>
const programIds = allProgramIds[cluster]
if (
!programIds?.securityTokenProgram ||
!programIds.transferHookProgram ||
!programIds.transferWhitelistProgram
) {
throw new Error(`Missing one or more program IDs for ${cluster} in ${configPath}`)
}
const securityTokenProgramId = address(programIds.securityTokenProgram)
const transferHookProgramId = address(programIds.transferHookProgram)
const verifierProgramId = address(programIds.transferWhitelistProgram)
// Inputs: payer signer, mint signer, and required PDAs.
// PDA derivation is the same as in scripts/issuer-setup.ts.
const initMintIx = getInitializeMintInstruction(
{
mint: mintSigner,
authority: mintAuthorityPda,
payer: payerSigner,
initializeMintArgs: {
ixMint: {
decimals: 6,
mintAuthority: payerSigner.address,
freezeAuthority: freezeAuthorityPda,
},
ixMetadataPointer: {
authority: payerSigner.address,
metadataAddress: mintSigner.address,
},
ixMetadata: {
name: "Example Security Token",
symbol: "EXMPL",
uri: "",
additionalMetadata: new Uint8Array(),
},
ixScaledUiAmount: null,
},
},
{ programAddress: securityTokenProgramId },
)
const initMintConfigIx = getInitializeVerificationConfigInstruction(
{
mint: mintSigner.address,
verificationConfigOrMintAuthority: mintAuthorityPda,
instructionsSysvarOrCreator: payerSigner.address,
payer: payerSigner,
mintAccount: mintSigner.address,
configAccount: verificationConfigMintPda,
systemProgram: address("11111111111111111111111111111111"),
accountMetasPda,
transferHookPda,
transferHookProgram: transferHookProgramId,
initializeVerificationConfigArgs: {
instructionDiscriminator: INSTRUCTION_DISCRIMINATORS.mint,
cpiMode: true,
programAddresses: [verifierProgramId],
},
},
{ programAddress: securityTokenProgramId },
)
const initTransferConfigIx = getInitializeVerificationConfigInstruction(
{
mint: mintSigner.address,
verificationConfigOrMintAuthority: mintAuthorityPda,
instructionsSysvarOrCreator: payerSigner.address,
payer: payerSigner,
mintAccount: mintSigner.address,
configAccount: verificationConfigTransferPda,
systemProgram: address("11111111111111111111111111111111"),
accountMetasPda,
transferHookPda,
transferHookProgram: transferHookProgramId,
initializeVerificationConfigArgs: {
instructionDiscriminator: INSTRUCTION_DISCRIMINATORS.transfer,
cpiMode: false, // introspection mode
programAddresses: [verifierProgramId],
},
},
{ programAddress: securityTokenProgramId },
)
const mintIx = getMintInstruction(
{
mint: mintSigner.address,
verificationConfig: verificationConfigMintPda,
instructionsSysvar: INSTRUCTIONS_SYSVAR,
mintAuthority: mintAuthorityPda,
mintAccount: mintSigner.address,
destination: recipientAta,
tokenProgram: TOKEN_2022_PROGRAM,
amount: 1_000n,
},
{ programAddress: securityTokenProgramId },
)
const mintWithVerifierIx = {
...mintIx,
accounts: [
...(mintIx.accounts ?? []),
{ address: verifierProgramId, role: AccountRole.READONLY },
],
}
const rpc = createSolanaRpc("https://api.devnet.solana.com")
const send = sendTransactionWithoutConfirmingFactory({ rpc })
for (const instruction of [initMintIx, initMintConfigIx, initTransferConfigIx, mintWithVerifierIx]) {
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send()
const message = appendTransactionMessageInstructions(
[instruction],
setTransactionMessageLifetimeUsingBlockhash(
latestBlockhash,
setTransactionMessageFeePayerSigner(payerSigner, createTransactionMessage({ version: "legacy" })),
),
)
const signed = await signTransactionMessageWithSigners(message)
await send(signed, { commitment: "confirmed" })
}
4) Use the full reference script for production
The snippet above is intentionally minimal. For production usage, start from scripts/issuer-setup.ts in ssts-example because it already handles:
- program account validation
- idempotent reruns
- airdrop/setup conveniences for devnet/localnet
- setup artifact output (
config/issuer-state-<cluster>.json)