Skip to content

Commit f774c06

Browse files
committed
feat(anchor): add transfer checked helper
1 parent 272d269 commit f774c06

6 files changed

Lines changed: 134 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to the Solana SDK Zig implementation will be documented in this file.
44

5+
### Session 2026-01-15-089
6+
7+
**Date**: 2026-01-15
8+
**Goal**: Transfer checked helper sugar
9+
10+
#### Completed Work
11+
1. Added `transferCheckedWithMint` helper
12+
2. Updated docs
13+
14+
#### Test Results
15+
- `./solana-zig/zig build test --summary all`
16+
517
### Session 2026-01-15-088
618

719
**Date**: 2026-01-15

anchor/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to sol-anchor-zig will be documented in this file.
44

5+
### Session 2026-01-15-017
6+
7+
**Date**: 2026-01-15
8+
**Goal**: Add transferChecked DSL sugar
9+
10+
#### Completed Work
11+
1. Added `transferCheckedWithMint` helper
12+
2. Updated docs
13+
14+
#### Test Results
15+
- `./solana-zig/zig build test --summary all`
16+
517
### Session 2026-01-15-016
618

719
**Date**: 2026-01-15

anchor/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,30 @@ fn send(ctx: anchor.Context(TransferAccounts), amount: u64) !void {
303303
}
304304
```
305305

306+
When the mint is already loaded, use `transferCheckedWithMint` to avoid
307+
passing decimals explicitly.
308+
309+
```zig
310+
const token = anchor.token;
311+
312+
fn sendChecked(ctx: anchor.Context(TransferAccounts), amount: u64) !void {
313+
if (token.transferCheckedWithMint(
314+
ctx.accounts.token_program.toAccountInfo(),
315+
ctx.accounts.source.toAccountInfo(),
316+
ctx.accounts.mint,
317+
ctx.accounts.destination.toAccountInfo(),
318+
ctx.accounts.authority.toAccountInfo(),
319+
amount,
320+
)) |err| switch (err) {
321+
.InvokeFailed => return error.InvokeFailed,
322+
.InvokeFailedWithCode => |code| {
323+
_ = code;
324+
return error.InvokeFailed;
325+
},
326+
};
327+
}
328+
```
329+
306330
For ATA init/payer semantics, use the ATA marker with `if_needed` (idempotent)
307331
and include the associated token program and system program accounts.
308332

anchor/ROADMAP.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# sol-anchor-zig Roadmap
22

3+
## ✅ v3.2.67 - Transfer Checked Sugar
4+
5+
- [x] Add `transferCheckedWithMint` helper for token CPI
6+
- [x] Update docs
7+
38
## ✅ v3.2.66 - ATA Init/Payer Semantics
49

510
- [x] Add associated token CPI helpers

anchor/src/token.zig

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,40 @@ fn resolveAccountInfo(comptime field_name: []const u8, accounts: anytype) *const
9393
@compileError("account field must be AccountInfo or type with toAccountInfo(): " ++ field_name);
9494
}
9595

96+
fn toAccountInfo(value: anytype) *const AccountInfo {
97+
const TargetType = @TypeOf(value);
98+
if (TargetType == AccountInfo) {
99+
@compileError("AccountInfo values are not supported; pass a pointer");
100+
}
101+
if (@typeInfo(TargetType) == .pointer) {
102+
const ChildType = @typeInfo(TargetType).pointer.child;
103+
if (ChildType == AccountInfo) {
104+
return value;
105+
}
106+
if (@hasDecl(ChildType, "toAccountInfo")) {
107+
return value.toAccountInfo();
108+
}
109+
}
110+
if (@hasDecl(TargetType, "toAccountInfo")) {
111+
return value.toAccountInfo();
112+
}
113+
@compileError("value must be AccountInfo pointer or type with toAccountInfo()");
114+
}
115+
116+
fn mintDecimals(value: anytype) u8 {
117+
const TargetType = @TypeOf(value);
118+
if (@hasField(TargetType, "data")) {
119+
return value.data.decimals;
120+
}
121+
if (@typeInfo(TargetType) == .pointer) {
122+
const ChildType = @typeInfo(TargetType).pointer.child;
123+
if (@hasField(ChildType, "data")) {
124+
return value.*.data.decimals;
125+
}
126+
}
127+
@compileError("mint account must expose data.decimals");
128+
}
129+
96130
// ============================================================================
97131
// TokenAccount Wrapper
98132
// ============================================================================
@@ -552,6 +586,28 @@ pub fn transferChecked(
552586
return invokeInstruction(&ix, infos[0..], null);
553587
}
554588

589+
/// Invoke SPL Token transferChecked using mint wrapper decimals.
590+
pub fn transferCheckedWithMint(
591+
token_program: *const AccountInfo,
592+
source: *const AccountInfo,
593+
mint_account: anytype,
594+
destination: *const AccountInfo,
595+
authority: *const AccountInfo,
596+
amount: u64,
597+
) ?TokenCpiError {
598+
const mint_info = toAccountInfo(mint_account);
599+
const decimals = mintDecimals(mint_account);
600+
return transferChecked(
601+
token_program,
602+
source,
603+
mint_info,
604+
destination,
605+
authority,
606+
amount,
607+
decimals,
608+
);
609+
}
610+
555611
/// Invoke SPL Token mintTo.
556612
pub fn mintTo(
557613
token_program: *const AccountInfo,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Story: v3.2.67 Transfer Checked DSL Sugar
2+
3+
> Add transfer_checked helper that infers decimals from mint wrapper.
4+
5+
## Goals
6+
7+
- Provide a convenience helper to avoid passing mint decimals explicitly.
8+
9+
## Acceptance Criteria
10+
11+
### anchor/src/token.zig
12+
13+
- [x] Add `transferCheckedWithMint` helper.
14+
- [x] Helper infers decimals from mint wrapper and uses mint account info.
15+
16+
### Documentation
17+
18+
- [x] Update README with new helper example.
19+
- [x] Update ROADMAP and CHANGELOG.
20+
21+
## Completion Status
22+
23+
- Start date: 2026-01-15
24+
- Completion date: 2026-01-15
25+
- Status: ✅ Completed

0 commit comments

Comments
 (0)