@cfxlabsinc/mintfx-programThe mintfx Anchor program (programs/mintfx), its TypeScript client (src/,
imported by subpath: ., ./generated, ./pda, ./uuid, ./events,
./token, ./state, ./search), and its test harness (./test, which deploys
the pinned .so in src/test-harness/fixtures).
src/generated/ is written by codama from the program's IDL when the program
builds, so regenerate it with nx run mintfx-program:build-program rather than
editing it. It is checked in, and the check-generated-client target (part of
test) fails when it differs from a fresh generation, so commit it with the
program change. It holds every instruction builder, so callers import those from
./generated. What is hand-written on top is PDA derivation, account decoders
and search filters, the event decoders, and the Token Metadata update. It makes
no RPC calls.
| Task | Command |
|---|---|
| Build the program | nx run mintfx-program:build-program |
| Every suite | nx run mintfx-program:test |
| Unit tests | nx run mintfx-program:test-unit |
Client, pinned .so |
nx run mintfx-program:test-client |
| Anchor suites | nx run mintfx-program:test-anchor (-c <suite> for one) |
| Cargo tests | nx run mintfx-program:test-cargo |
| Clippy / rustfmt | nx run mintfx-program:clippy, rustfmt |
| Refresh test fixtures | nx run mintfx-program:update-test-fixtures |
| Verifiable build | nx run mintfx-program:build-verifiable -c dev|prod |
| Typecheck | nx run mintfx-program:typecheck |
Import the builders from @cfxlabsinc/mintfx-program/generated and pass
{ programAddress }. It is required: the program id is chosen per environment
when the program is built, so nothing in the client bakes one in.
MINTFX_ADDRESSES has the three.
adminAuthority is required for the same reason, on the ten builders that take
it (createAuthority, updateAuthority, destroyAuthority, createToken,
createToken2022, createMetadata, the three create*EscrowForToken, and
reclaimExcessLamports). It used to be optional, defaulting to the address
Anchor wrote into the IDL from #[account(address = ADMIN_AUTHORITY)] — but
that address is env!("MINTFX_ADMIN_AUTHORITY_ADDRESS"), resolved at build time,
and this package is generated from a localnet build and shared by all
three environments. So the default was the localnet authority on dev and
mainnet, and a caller who omitted it got a meta that is neither a signer nor the
right key: a failure at signing, or ConstraintAddress on chain. codama.mjs
drops the default (scripts/require-admin-authority.mjs), so omitting it
is now a compile error rather than a wrong key.
import { MINTFX_ADDRESSES } from "@cfxlabsinc/mintfx-program";
import { getMintInstructionAsync } from "@cfxlabsinc/mintfx-program/generated";
import { deterministicUUIDBytes } from "@cfxlabsinc/mintfx-program/uuid";
const programAddress = MINTFX_ADDRESSES.mainnet;
const instruction = await getMintInstructionAsync(
{
authority,
payer,
tokenId,
ik: deterministicUUIDBytes(ik),
targetAmounts,
targetAccountsStartIdx: 0,
},
{ programAddress },
);
The builders derive every account they can, so you pass the signers and the arguments. What they cannot know is left to the caller:
Token accounts: the account a wallet is debited from (ledgerAccount on
redeem and withdraw, sourceLedgerAccount on transfer,
sourceTokenAccount on redeem_external and withdraw_external) is an input.
The program takes any token account of the mint that the wallet owns, so
nothing assumes the associated one. getLedgerAccountATAAddress from ./pda
derives the conventional one.
Ids: ik and every wallet id are 16 bytes. Convert a uuid with
deterministicUUIDBytes from ./uuid.
Amounts: integers in base units, so scale by 10 ** decimals yourself.
Remaining accounts: append them after the builder's accounts, in the order of the amounts, with a start index of 0 unless you put your own accounts first.
mint, commit_withdrawal: the target token accounts, writable.commit_redeem: the fee token accounts, writable.transfer: each target wallet's authority PDA, readonly, then each target's
token account, writable. The token account must be of the mint and owned by
that target's authority PDA; it need not be the associated one. The bumps
argument is the authorities' bumps, from getLedgerAccountAuthorityWithBump
in ./pda. Bumps for the associated token accounts may follow them, as older
clients sent, but the program no longer reads them.Permissions: create_authority takes every flag. Spread your permissions
over DEFAULT_EMPTY_PERMISSIONS from ./state.
Refund destination: void_withdrawal and void_redeem refund to the token
account you pass as refundDestination; nothing derives it, because where a
refund goes is a decision. For a ledger wallet's own account, derive it with
getLedgerAccountATAAddress from ./pda and pass the wallet id as
refundWalletId, which the program checks against the destination's owner.
For any other destination pass refundWalletId: null.
packages/mintfx-program/tests/instructions.ts builds instructions this way;
copy from it.
The client is stateless. It builds instructions, derives PDAs and decodes bytes, and never calls an RPC endpoint, so the caller decides how, when and how often to read.
./pda), fetch it with the generated
fetchers (./generated, for example fetchRedemptionInfo), then shape it
with the pure parsers in ./state (parseRedemptionInfoAccountData,
parseWithdrawalInfoAccountData, parseAuthorityAccountData).getProgramAccounts with
getAccountKindFilter(kind) from ./search, then decodeAccountOfKind. Use
getAccountKind to name an account you fetched without knowing its kind.parseCPIEvents
(./events). getIdempotentActionAddress names the account that records an
action, and the history of transactions that touched it is where its events
live.getMintMetadataAddress (./pda) and
decodeTokenMetadataAccount (./token).packages/mintfx-program/tests/reads.ts implements all of that, with the names
and signatures the client used to export. Copy it into your own code.
The generated get*InstructionAsync builders derive every PDA account, plus
eventAuthority and program, so a caller passes the arguments (tokenId,
ik, ...) and the signers, never the addresses. Two things in codama.mjs make
that possible:
@codama/nodes-from-anchor skips any seed whose
path is nested, and ours are all args.token_id. So each instruction's single
args struct is first flattened into top-level arguments
(scripts/flatten-idl-args.mjs). Borsh writes a struct as its fields
back to back, so the instruction data is byte for byte the same.#[event_cpi] accounts as bare names, so their defaults
are declared there: the PDA of the constant seed __event_authority, and the
program's own id.pda.ts exports friendly names for reading an address without building an
instruction: each hashes string ids with deterministicUUIDBytes and calls the
generated find*Pda finder, so there is one derivation per PDA. Codama names
colliding PDAs after the first instruction that uses them, so codama.mjs
(canonicalizePdas) merges the duplicates and gives each finder a stable name.
tests/client/pdaParity.spec.ts pins every address to a known-good value.
The hand-written instruction helpers are gone. They were thin mappers over the generated builders (a uuid to bytes, an amount scaled, some remaining accounts), so callers use the builders directly; see "Building instructions".
| Entry point | Removed | Use instead |
|---|---|---|
./mint |
createMintInstruction |
getMintInstructionAsync |
./transfer |
createTransferInstruction |
getTransferInstructionAsync |
./redeem |
createRedeemInstruction, createCommitRedeemInstruction, createVoidRedeemInstruction |
getRedeemInstructionAsync, getCommitRedeemInstructionAsync, getVoidRedeemInstructionAsync |
./withdraw |
createWithdrawInstruction, createCommitWithdrawalInstruction, createVoidWithdrawalInstruction |
getWithdrawInstructionAsync, getCommitWithdrawalInstructionAsync, getVoidWithdrawalInstructionAsync |
./rbac |
createAuthorityInstruction, updateAuthorityInstruction, destroyAuthorityInstruction |
getCreateAuthorityInstructionAsync, getUpdateAuthorityInstructionAsync, getDestroyAuthorityInstructionAsync |
./token |
createTokenInstruction, createToken2022Instruction, pauseTokenInstruction, unpauseTokenInstruction |
getCreateTokenInstructionAsync, getCreateToken2022InstructionAsync, getPauseTokenInstructionAsync, getUnpauseTokenInstructionAsync |
./token |
createEscrowInstruction |
getCreateMintEscrowForTokenInstructionAsync, getCreateRedemptionEscrowForTokenInstructionAsync, getCreateWithdrawalEscrowForTokenInstructionAsync |
./token |
createTokenMetadataInstruction |
getCreateMetadataInstructionAsync. It also fetched the metadata URI in the background, best effort and never awaited; that is gone, so the client does no I/O at all |
The ./mint, ./transfer, ./redeem, ./withdraw and ./rbac entry points no
longer exist. ./token keeps updateTokenMetadataInstruction, which builds a
Token Metadata program instruction that is not in this program's IDL, and
decodeTokenMetadataAccount. The permission types and parsers were already
exported from ./state and still are. The DEFAULT_*_START_INDEX constants are
gone: the start index is 0.
./pda — ACCESS_CONTROL_SEED, EVENT_AUTHORITY_SEED, MINT_ESCROW_SEED,
MINT_IK_SEED, REDEEM_IK_SEED, REDEMPTION_ESCROW_SEED,
TOKEN_INFO_SEED, TRANSFER_IK_SEED, WITHDRAW_IK_SEED,
WITHDRAWAL_ESCROW_SEED.Changed with the program:
./generated — withdraw_external is new. It debits an external wallet
that signs the transaction, alongside the operator, instead of a ledger
account. It uses the same permission as withdraw, and an IK is the same
account either way, so commit_withdrawal and void_withdrawal handle both.
It emits WithdrawEvent, as withdraw does../generated — void_withdrawal no longer assumes the refund goes back to
whoever initiated the withdrawal. Breaking: refundLedgerAccountAuthority
and refundLedgerAccountTokenAccount are replaced by one required
refundDestination, the token account to credit, and refundWalletId is
optional. Passing null is for a destination that is not a ledger wallet's:
an external wallet, or an account held for review. It takes VoidWithdrawal
whatever the destination, so that permission now decides where escrowed funds
go, as CommitWithdrawal does. The IK record does not change and still names
the initiator../events — VoidWithdrawalEvent has refundWalletId as an option and a
new refundDestination: the token account credited, always present. It is the
initiator's own account unless the void was directed elsewhere, so compare it
with the initiator's before treating a void as a refund to the customer. The
previous shape still decodes../generated — redeem_external is new, and void_redeem changes exactly
as void_withdrawal does: a required refundDestination replaces
refundLedgerAccountAuthority and refundLedgerAccountTokenAccount,
refundWalletId is optional, and VoidRedeem alone decides where the refund
goes. redeem_external debits an external wallet that co-signs with the
operator, uses the Redeem permission, and shares the redeem IK with
redeem, so commit_redeem and void_redeem handle both. It emits
RedeemEvent, as redeem does, and VoidRedeemEvent gains
refundDestination and an optional refundWalletId, with the previous shape
still decodable../events — Breaking: WithdrawExternalEvent and RedeemExternalEvent
are gone, and WithdrawEvent / RedeemEvent carry a new
sourceAuthority: Option<Address> instead: Some(wallet) from the external
instruction, None from the ledger-funded one. MintfxEvent loses the two
union members, so a caller that named them — in a list of event names to
match, or a cast — has to drop them and read sourceAuthority to tell the two
fundings apart. The two names were never emitted on devnet or mainnet, so they
get no legacy layout; the pre-sourceAuthority shape of the two base events
does, and still decodes with the field names it always had../generated — findRefundLedgerAccountAuthorityPda is gone. Neither void
instruction derives a refund authority any more.Everything the client used to read from chain is gone, along with the instructions the program no longer has.
Removed because they made RPC calls:
| Entry point | Removed | Use instead |
|---|---|---|
./state |
getParsedRedemptionInfo, getParsedRedemptionInfoFromAddress, getParsedWithdrawalInfo, getParsedWithdrawalInfoFromAddress, getParsedAuthority, getParsedAuthorityFromAddress, fetchTokenInfoByTokenId |
reads.ts; the parse*AccountData parsers stay, and tokenIdFromBytes is new |
./state |
checkAuthorityPermissions, getParsedRedemptionIk, getParsedRedemptionIkFromAddress, RedemptionIK, parseRedemptionIKAccountData |
nothing called them |
./search |
listAccountsOfKind, listAllProgramAccounts, listAuthorities, listRedemptions |
getAccountKindFilter, getAccountKind, decodeAccountOfKind |
./events |
getCPIEventsFromTransaction, findCompletedIdempotentAction |
parseCPIEvents and getIdempotentActionAddress |
./token |
fetchMaybeTokenMetadata, updateToken2022MetadataInstructions |
decodeTokenMetadataAccount; for Token-2022, use getUpdateTokenMetadataFieldInstruction from @solana-program/token-2022 |
Removed with the program's own changes:
./redeem — createLedgerAccountInstruction. A wallet is a token account
its signing PDA owns, conventionally the associated one, so create it with the
associated token program../state, ./rbac — the canCreateLedgerAccount permission. Bits 6 and 7
are retired and must not be reused. An authority that still holds bit 6 fails
every permissioned instruction until the admin rewrites it with
update_authority../generated — the create_ledger_account and
close_idempotent_mint_account instructions. update_authority's
newPermissions and UpdateAuthorityEvent.permissions are no longer
optional.CreateLedgerAccountEvent and CloseIdempotentMintAccountEvent are retired but
still decodable, like every other retired event.
The migrations these served are finished on devnet and mainnet, so 2.4.2
removed them — as a patch, because its commit was scoped to mintfx-program
and nx release attributes a scoped commit to the project of that name. 3.0.0
carries the same code under the version the removal always warranted. Treat
2.4.2 as 3.0.0 when reading the history.
Gone from the public API:
./rbac — recreateAuthorityInstructions, RecreateAuthorityInstructionsInput.
getParsedAuthority and getParsedAuthorityFromAddress no longer throw on an
account that is not 48 bytes; no such account remains../pda — ACCESS_CONTROL_SEED_V1, MINT_SWAP_WALLET_SEED,
getMintSwapInputWalletAuthority, getMintSwapInputWalletAddress../search — WALLET_AUTHORITY_KINDS. MintfxAccountKind drops
ledgerAccount, redemptionWallet and mintSwapWallet, so
listAllProgramAccounts reports kind: null for them, and authorities are no
longer filtered by data size../generated — the builders, parsers, codecs and types for
sweep_mint_swap_wallet, close_mint_swap_wallet,
close_ledger_account_authority and migrate_redemption_wallet, the
MintSwapWallet account helpers, and their event codecs. The
MintfxInstruction and MintfxAccount enum values renumber.Still decodable: every retired event keeps a hand-written decoder in
src/program/legacyEvents.ts, so historical transactions parse unchanged.
ReclaimExcessLamportsEvent decodes by name now instead of coming back
undecodable. AccountRequiresMigration stays in the error enum as a retired
variant, so NotProgramOwned keeps code 6014.