@unclaimedsol/spl-burn-close-sdk 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +103 -6
- package/dist/index.cjs +39 -26
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +37 -26
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
- Add `buildCloseEmptyTokenAccountsInstruction` and
|
|
6
|
+
`buildCloseEmptyTokenAccountsTransactions` for opt-in execution-time empty-account
|
|
7
|
+
checks. They emit exact data `[2]` with the Rust program's existing token account
|
|
8
|
+
layout, support classic SPL Token and Token-2022, and never fall back to burning.
|
|
9
|
+
- Reuse unsigned batching, `BuildOptions`, blockhash/expiry handling, fee payer
|
|
10
|
+
support and the 1,232-byte limit. The instruction helper makes no RPC calls.
|
|
11
|
+
- Verify the ABI against Rust commit `1f2b10ae668c47de97627de69cdf656878ed9b0e`
|
|
12
|
+
and its deterministic fixture. Extend regression, browser-data and packed-package
|
|
13
|
+
coverage across CJS, ESM and TypeScript bundler resolution.
|
|
14
|
+
- Document deployment prerequisites, skipped targets, funded/unsynchronized WSOL
|
|
15
|
+
exclusion, unchanged fees and the limits of preview and signature-based reporting.
|
|
16
|
+
|
|
17
|
+
Existing burn-and-close APIs, selector `[0]`, defaults and behavior are preserved.
|
|
18
|
+
Consumers must explicitly switch builders after the program upgrade is deployed;
|
|
19
|
+
an SDK dependency update alone does not protect existing cleanup calls. This release
|
|
20
|
+
does not add checked buffer approvals or guarantee exact preview proceeds.
|
|
21
|
+
|
|
3
22
|
## 1.3.0
|
|
4
23
|
|
|
5
24
|
- Preserve the fetched blockhash's `lastValidBlockHeight` on built transactions and add
|
package/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# Burn and Close Solana SPL Tokens SDK
|
|
2
2
|
|
|
3
|
-
A lightweight Solana SDK
|
|
3
|
+
A lightweight Solana SDK for intentional token burning and opt-in empty-token-account cleanup, with the existing Unclaimed SOL token cleanup fee policy.
|
|
4
4
|
This library only **builds unsigned transactions** – you handle signing and sending them, whether in the browser (Wallet Adapter) or server (Keypair).
|
|
5
5
|
|
|
6
|
+
`buildBurnAndCloseInstruction` and `buildBurnAndCloseTransactions` intentionally burn
|
|
7
|
+
and close (selector 0). They keep their existing behavior, including burning tokens
|
|
8
|
+
that arrive after an RPC balance check. Updating this dependency alone does not
|
|
9
|
+
change existing callers.
|
|
10
|
+
|
|
11
|
+
`buildCloseEmptyTokenAccountsInstruction` and `buildCloseEmptyTokenAccountsTransactions`
|
|
12
|
+
request execution-time empty-account checks (selector 2) and never burn. The program
|
|
13
|
+
upgrade must be deployed before consumers use these builders. This SDK does not
|
|
14
|
+
probe deployment or fall back to burn-and-close if selector 2 is unsupported.
|
|
15
|
+
|
|
6
16
|
## ✨ Features
|
|
7
17
|
|
|
8
18
|
- 🔥 Burn all tokens in provided ATAs.
|
|
9
19
|
- 🧹 Close token accounts, reclaiming rent to the user.
|
|
20
|
+
- Opt-in empty-account cleanup that preserves tokens deposited after a preview.
|
|
10
21
|
- 💸 On-chain fee recipient is enforced by the program.
|
|
11
22
|
- 🧾 Hybrid cleanup fee: `max(floor(gross * 200 / 10000), gross - 2,000,000)` lamports per closed account. For 2,039,280 lamports recovered, the fee is 40,785 lamports.
|
|
12
23
|
- ⚡ Automatic batching (default 12 ATAs per transaction).
|
|
@@ -18,8 +29,9 @@ This library only **builds unsigned transactions** – you handle signing and se
|
|
|
18
29
|
|
|
19
30
|
## 🧩 How It Works
|
|
20
31
|
|
|
21
|
-
1. The SDK prepares
|
|
22
|
-
2.
|
|
32
|
+
1. The SDK prepares instructions for the selected cleanup path.
|
|
33
|
+
2. The legacy path burns and closes. The new close-empty path closes only eligible
|
|
34
|
+
empty accounts, returning their lamports to the wallet without burning tokens.
|
|
23
35
|
3. The program sends the greater of 2% of recovered rent (rounded down) or the amount above 2,000,000 lamports to the hardcoded fee recipient, per successfully closed account.
|
|
24
36
|
4. The SDK never asks for private keys — you sign & send transactions yourself.
|
|
25
37
|
|
|
@@ -41,7 +53,72 @@ that breaks CommonJS loading. CI tests the repository’s locked dependency grap
|
|
|
41
53
|
|
|
42
54
|
## 🔑 Usage
|
|
43
55
|
|
|
44
|
-
###
|
|
56
|
+
### Empty-account cleanup (requires the program upgrade)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Connection, PublicKey } from "@solana/web3.js";
|
|
60
|
+
import {
|
|
61
|
+
buildCloseEmptyTokenAccountsInstruction,
|
|
62
|
+
buildCloseEmptyTokenAccountsTransactions,
|
|
63
|
+
PROGRAM_ID,
|
|
64
|
+
TOKEN_PROGRAM_ID,
|
|
65
|
+
} from "@unclaimedsol/spl-burn-close-sdk";
|
|
66
|
+
|
|
67
|
+
const connection = new Connection("https://api.mainnet-beta.solana.com");
|
|
68
|
+
declare const user: PublicKey;
|
|
69
|
+
declare const pairs: { mint: PublicKey; ata: PublicKey }[];
|
|
70
|
+
|
|
71
|
+
// Instruction only: no RPC calls. Returns null for an empty list.
|
|
72
|
+
const ix = await buildCloseEmptyTokenAccountsInstruction(
|
|
73
|
+
PROGRAM_ID, user, pairs, TOKEN_PROGRAM_ID
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Unsigned batches: fetches one blockhash unless supplied in BuildOptions.
|
|
77
|
+
// Returns [] for an empty list. The default token program is classic SPL Token.
|
|
78
|
+
const txs = await buildCloseEmptyTokenAccountsTransactions(
|
|
79
|
+
connection, PROGRAM_ID, user, pairs, { chunkSize: 12 }, TOKEN_PROGRAM_ID
|
|
80
|
+
);
|
|
81
|
+
// Sign and send using your wallet only after confirming upgrade availability.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use `TOKEN_2022_PROGRAM_ID` for Token-2022. Each instruction uses one token program;
|
|
85
|
+
group accounts by program and build separate instructions or batches. `Pair.ata`
|
|
86
|
+
can also name a non-ATA token account. Both transaction builders use the same
|
|
87
|
+
`BuildOptions`, expiry metadata, fee payer defaults and packet-size checks below.
|
|
88
|
+
|
|
89
|
+
Eligibility is enforced by the upgraded program at execution, not by SDK RPC
|
|
90
|
+
balance checks. Ineligible targets may be skipped while eligible siblings close;
|
|
91
|
+
an entirely skipped batch may succeed. A built transaction or successful signature
|
|
92
|
+
alone does not prove every requested account closed. Check resulting account state
|
|
93
|
+
and execution results before reporting recovered amounts.
|
|
94
|
+
|
|
95
|
+
Funded WSOL, including unsynchronized SOL deposits, is excluded. A native account
|
|
96
|
+
must have zero token amount and lamports exactly equal to its recorded reserve.
|
|
97
|
+
The program does not burn, unwrap funded principal, or call `SyncNative` on this path.
|
|
98
|
+
Token-2022 support is conservative: supported account extensions are `ImmutableOwner`
|
|
99
|
+
and `TransferFeeAmount`, with `TransferFeeConfig` on mints. Other extensions are
|
|
100
|
+
skipped. Eligible withheld transfer fees may be harvested to the mint before closure.
|
|
101
|
+
|
|
102
|
+
The existing per-account fee policy applies only to successful closures; skipped
|
|
103
|
+
targets incur no service fee, though network transaction fees still apply. This
|
|
104
|
+
change addresses empty-token cleanup. It does not introduce checked buffer approvals
|
|
105
|
+
or guarantee exact preview proceeds. Buffer, reward and stake paths are outside this API.
|
|
106
|
+
|
|
107
|
+
Selector 2 validates effective transaction-wide privileges. Keep the fee recipient,
|
|
108
|
+
mints and token accounts nonsigners, and the token/system programs read-only. A
|
|
109
|
+
custom fee payer must not alias these accounts; the signing wallet may be the payer
|
|
110
|
+
but cannot also be the fee recipient. Instructions added to the same transaction
|
|
111
|
+
must preserve these requirements.
|
|
112
|
+
|
|
113
|
+
The ABI is verified against Rust commit
|
|
114
|
+
[`1f2b10ae668c47de97627de69cdf656878ed9b0e`](https://github.com/nedim1511/unclaimed-sol-close-token-program/commit/1f2b10ae668c47de97627de69cdf656878ed9b0e),
|
|
115
|
+
its [instruction specification](https://github.com/nedim1511/unclaimed-sol-close-token-program/blob/1f2b10ae668c47de97627de69cdf656878ed9b0e/docs/close-empty-token-accounts.md)
|
|
116
|
+
and [deterministic fixture](https://github.com/nedim1511/unclaimed-sol-close-token-program/blob/1f2b10ae668c47de97627de69cdf656878ed9b0e/tests/fixtures/close_empty_token_accounts.json).
|
|
117
|
+
Exact data is `[2]`; metas are writable fee recipient, writable signing wallet,
|
|
118
|
+
read-only token and system programs, then writable mint/token-account pairs.
|
|
119
|
+
Source and local runtime verification do not establish deployment.
|
|
120
|
+
|
|
121
|
+
### Intentional burning: signing with Keypair
|
|
45
122
|
|
|
46
123
|
```typescript
|
|
47
124
|
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
@@ -79,7 +156,7 @@ for (const tx of txs) {
|
|
|
79
156
|
}
|
|
80
157
|
```
|
|
81
158
|
|
|
82
|
-
###
|
|
159
|
+
### Intentional burning: signing with Wallet Adapter
|
|
83
160
|
|
|
84
161
|
```typescript
|
|
85
162
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
@@ -167,6 +244,26 @@ do not call Buffer-only methods on it.
|
|
|
167
244
|
This SDK targets the Unclaimed SOL mainnet program (`PROGRAM_ID`) and fee recipient (`FEE_RECIPIENT`).
|
|
168
245
|
Transactions will only succeed on a cluster where that program is deployed.
|
|
169
246
|
|
|
247
|
+
## Verification
|
|
248
|
+
|
|
249
|
+
Run `npm test` for encoding, unsigned transaction behavior and legacy regressions.
|
|
250
|
+
Run `npm run test:package` to pack the actual package, verify its contents, load
|
|
251
|
+
CJS/ESM exports and compile consumers with Node16 and bundler type resolution.
|
|
252
|
+
|
|
253
|
+
For local runtime evidence, run `npm run test:runtime` with the Rust repository
|
|
254
|
+
at `../unclaimed-sol-rust-program`, its verified SBF artifact already built, and
|
|
255
|
+
`solana-test-validator` installed. An alternative Rust checkout can be supplied
|
|
256
|
+
with `npm run test:runtime -- /path/to/unclaimed-sol-rust-program`.
|
|
257
|
+
The harness checks the pinned Rust commit and artifact hash, starts an isolated
|
|
258
|
+
validator on local ports 18899–18940, and uses generated test keys and synthetic
|
|
259
|
+
accounts. It verifies closures, fees, late deposits, WSOL exclusions and legacy
|
|
260
|
+
burning for both token programs. Temporary ledger/log files are retained at the
|
|
261
|
+
printed path. The runtime harness is excluded from the published package.
|
|
262
|
+
|
|
263
|
+
Local runtime success does not verify a deployed cluster or certify future token
|
|
264
|
+
program versions. See [the 1.4.0 release handoff](https://github.com/unclaimed-sol/spl-burn-close-sdk/blob/main/docs/release-1.4.0.md)
|
|
265
|
+
in the source repository for evidence limits and MCP adoption steps.
|
|
266
|
+
|
|
170
267
|
---
|
|
171
268
|
|
|
172
269
|
## 💻 Demo
|
|
@@ -177,7 +274,7 @@ There is a nice tutorial guide with a demo repository that shows the usage of th
|
|
|
177
274
|
|
|
178
275
|
## 📜 Disclaimer
|
|
179
276
|
|
|
180
|
-
This SDK only **builds transactions**
|
|
277
|
+
This SDK only **builds instructions and unsigned transactions** for the selected cleanup path. You are solely responsible for signing and sending them. We are not liable for any loss of funds.
|
|
181
278
|
|
|
182
279
|
---
|
|
183
280
|
|
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TOKEN_2022_PROGRAM_ID = exports.TOKEN_PROGRAM_ID = exports.FEE_RECIPIENT = exports.PROGRAM_ID = void 0;
|
|
4
4
|
exports.buildBurnAndCloseTransactions = buildBurnAndCloseTransactions;
|
|
5
|
+
exports.buildCloseEmptyTokenAccountsTransactions = buildCloseEmptyTokenAccountsTransactions;
|
|
5
6
|
exports.buildBurnAndCloseInstruction = buildBurnAndCloseInstruction;
|
|
7
|
+
exports.buildCloseEmptyTokenAccountsInstruction = buildCloseEmptyTokenAccountsInstruction;
|
|
6
8
|
const web3_js_1 = require("@solana/web3.js");
|
|
7
9
|
/** Exporting on-chain program ID for convenience */
|
|
8
10
|
exports.PROGRAM_ID = new web3_js_1.PublicKey("UNCaXzXkR3vp8mbCJyxWUvwuRk5uHgzrwe6jcWPfiUR");
|
|
@@ -12,8 +14,8 @@ exports.TOKEN_PROGRAM_ID = new web3_js_1.PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCW
|
|
|
12
14
|
exports.TOKEN_2022_PROGRAM_ID = new web3_js_1.PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
|
|
13
15
|
/** Legacy transaction wire limit, including the signature-count prefix and signature slots. */
|
|
14
16
|
const PACKET_DATA_SIZE = 1232;
|
|
15
|
-
function packetSizeError() {
|
|
16
|
-
return new Error(
|
|
17
|
+
function packetSizeError(operation) {
|
|
18
|
+
return new Error(`${operation}: transaction could not be serialized within the 1,232-byte limit. ` +
|
|
17
19
|
"Reduce chunkSize.");
|
|
18
20
|
}
|
|
19
21
|
/** web3.js reports oversized messages as "Transaction too large" or a buffer overrun. */
|
|
@@ -34,9 +36,22 @@ function isPacketSizeError(error) {
|
|
|
34
36
|
* @returns Array of *unsigned* Transaction objects
|
|
35
37
|
*/
|
|
36
38
|
async function buildBurnAndCloseTransactions(connection, programId, user, pairs, options = {}, tokenProgramId = exports.TOKEN_PROGRAM_ID) {
|
|
39
|
+
return buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, 0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build unsigned transactions requesting execution-time empty-account checks.
|
|
43
|
+
* Requires the upgraded program's selector 2. It never burns or falls back to
|
|
44
|
+
* burn-and-close. Ineligible targets can be skipped, including funded WSOL.
|
|
45
|
+
* Only a blockhash is fetched; the program enforces account eligibility.
|
|
46
|
+
*/
|
|
47
|
+
async function buildCloseEmptyTokenAccountsTransactions(connection, programId, user, pairs, options = {}, tokenProgramId = exports.TOKEN_PROGRAM_ID) {
|
|
48
|
+
return buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, 2);
|
|
49
|
+
}
|
|
50
|
+
async function buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, selector) {
|
|
51
|
+
const operation = selector === 0 ? "Burn and close" : "Close empty token accounts";
|
|
37
52
|
const chunkSize = options.chunkSize ?? 12;
|
|
38
53
|
if (!Number.isFinite(chunkSize) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
39
|
-
throw new Error(
|
|
54
|
+
throw new Error(`${operation}: chunkSize must be a finite positive integer.`);
|
|
40
55
|
}
|
|
41
56
|
if (!pairs.length)
|
|
42
57
|
return [];
|
|
@@ -47,30 +62,14 @@ async function buildBurnAndCloseTransactions(connection, programId, user, pairs,
|
|
|
47
62
|
for (let i = 0; i < pairs.length; i += chunkSize) {
|
|
48
63
|
chunks.push(pairs.slice(i, i + chunkSize));
|
|
49
64
|
}
|
|
50
|
-
// Instruction data: [selector=0]
|
|
51
65
|
// Use Uint8Array (browser-safe) and cast for web3.js' Buffer typing.
|
|
52
|
-
const data = new Uint8Array([
|
|
66
|
+
const data = new Uint8Array([selector]);
|
|
53
67
|
const { blockhash, lastValidBlockHeight } = options.recentBlockhash !== undefined
|
|
54
68
|
? { blockhash: options.recentBlockhash, lastValidBlockHeight: options.lastValidBlockHeight }
|
|
55
69
|
: await connection.getLatestBlockhash({ commitment });
|
|
56
70
|
const txs = [];
|
|
57
71
|
for (const chunk of chunks) {
|
|
58
|
-
const
|
|
59
|
-
// NOTE: Mark the user as signer in the instruction meta;
|
|
60
|
-
// the caller must actually sign the Transaction later.
|
|
61
|
-
{ pubkey: exports.FEE_RECIPIENT, isSigner: false, isWritable: true },
|
|
62
|
-
{ pubkey: user, isSigner: true, isWritable: true },
|
|
63
|
-
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
|
|
64
|
-
{ pubkey: web3_js_1.SystemProgram.programId, isSigner: false, isWritable: false },
|
|
65
|
-
];
|
|
66
|
-
for (const { mint, ata } of chunk) {
|
|
67
|
-
keys.push({ pubkey: mint, isSigner: false, isWritable: true }, { pubkey: ata, isSigner: false, isWritable: true });
|
|
68
|
-
}
|
|
69
|
-
const ix = new web3_js_1.TransactionInstruction({
|
|
70
|
-
programId,
|
|
71
|
-
keys,
|
|
72
|
-
data,
|
|
73
|
-
});
|
|
72
|
+
const ix = tokenAccountInstruction(programId, user, chunk, tokenProgramId, data);
|
|
74
73
|
const tx = new web3_js_1.Transaction().add(ix);
|
|
75
74
|
tx.feePayer = feePayer;
|
|
76
75
|
tx.recentBlockhash = blockhash;
|
|
@@ -81,13 +80,13 @@ async function buildBurnAndCloseTransactions(connection, programId, user, pairs,
|
|
|
81
80
|
}
|
|
82
81
|
catch (error) {
|
|
83
82
|
if (isPacketSizeError(error)) {
|
|
84
|
-
throw packetSizeError();
|
|
83
|
+
throw packetSizeError(operation);
|
|
85
84
|
}
|
|
86
|
-
throw new Error(
|
|
85
|
+
throw new Error(`${operation}: transaction could not be serialized. ` +
|
|
87
86
|
"Check the supplied transaction inputs, including recentBlockhash and account keys.", { cause: error });
|
|
88
87
|
}
|
|
89
88
|
if (wireSize > PACKET_DATA_SIZE) {
|
|
90
|
-
throw packetSizeError();
|
|
89
|
+
throw packetSizeError(operation);
|
|
91
90
|
}
|
|
92
91
|
txs.push(tx);
|
|
93
92
|
}
|
|
@@ -107,8 +106,22 @@ async function buildBurnAndCloseInstruction(programId, user, pairs, tokenProgram
|
|
|
107
106
|
if (!pairs.length) {
|
|
108
107
|
return null;
|
|
109
108
|
}
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
return tokenAccountInstruction(programId, user, pairs, tokenProgramId, new Uint8Array([0]));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build one close-empty instruction for SPL Token or Token-2022, without RPC.
|
|
113
|
+
* Returns null for empty input. Requires deployed selector 2 support; eligibility
|
|
114
|
+
* is checked by the program at execution, and ineligible targets may be skipped.
|
|
115
|
+
* This path never burns, including when tokens arrive after a preview.
|
|
116
|
+
* Data is a browser-safe Uint8Array typed as Buffer; avoid Buffer-only methods.
|
|
117
|
+
*/
|
|
118
|
+
async function buildCloseEmptyTokenAccountsInstruction(programId, user, pairs, tokenProgramId) {
|
|
119
|
+
if (!pairs.length) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return tokenAccountInstruction(programId, user, pairs, tokenProgramId, new Uint8Array([2]));
|
|
123
|
+
}
|
|
124
|
+
function tokenAccountInstruction(programId, user, pairs, tokenProgramId, data) {
|
|
112
125
|
// accounts order MUST match the on-chain program's expectation:
|
|
113
126
|
// [0] fee_recipient_account (your fee recipient)
|
|
114
127
|
// [1] user_wallet_account (the signer)
|
package/dist/index.d.cts
CHANGED
|
@@ -40,6 +40,13 @@ export type BuildOptions = {
|
|
|
40
40
|
* @returns Array of *unsigned* Transaction objects
|
|
41
41
|
*/
|
|
42
42
|
export declare function buildBurnAndCloseTransactions(connection: Connection, programId: PublicKey, user: PublicKey, pairs: Pair[], options?: BuildOptions, tokenProgramId?: PublicKey): Promise<Transaction[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Build unsigned transactions requesting execution-time empty-account checks.
|
|
45
|
+
* Requires the upgraded program's selector 2. It never burns or falls back to
|
|
46
|
+
* burn-and-close. Ineligible targets can be skipped, including funded WSOL.
|
|
47
|
+
* Only a blockhash is fetched; the program enforces account eligibility.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildCloseEmptyTokenAccountsTransactions(connection: Connection, programId: PublicKey, user: PublicKey, pairs: Pair[], options?: BuildOptions, tokenProgramId?: PublicKey): Promise<Transaction[]>;
|
|
43
50
|
/**
|
|
44
51
|
* Build a single **TransactionInstruction** that calls the on-chain
|
|
45
52
|
* burn+close program for a specific Token Program (either SPL Token or Token-2022).
|
|
@@ -51,3 +58,11 @@ export declare function buildBurnAndCloseTransactions(connection: Connection, pr
|
|
|
51
58
|
* do not rely on Buffer-only methods on the returned data.
|
|
52
59
|
*/
|
|
53
60
|
export declare function buildBurnAndCloseInstruction(programId: PublicKey, user: PublicKey, pairs: Pair[], tokenProgramId: PublicKey): Promise<TransactionInstruction | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Build one close-empty instruction for SPL Token or Token-2022, without RPC.
|
|
63
|
+
* Returns null for empty input. Requires deployed selector 2 support; eligibility
|
|
64
|
+
* is checked by the program at execution, and ineligible targets may be skipped.
|
|
65
|
+
* This path never burns, including when tokens arrive after a preview.
|
|
66
|
+
* Data is a browser-safe Uint8Array typed as Buffer; avoid Buffer-only methods.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildCloseEmptyTokenAccountsInstruction(programId: PublicKey, user: PublicKey, pairs: Pair[], tokenProgramId: PublicKey): Promise<TransactionInstruction | null>;
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,13 @@ export type BuildOptions = {
|
|
|
40
40
|
* @returns Array of *unsigned* Transaction objects
|
|
41
41
|
*/
|
|
42
42
|
export declare function buildBurnAndCloseTransactions(connection: Connection, programId: PublicKey, user: PublicKey, pairs: Pair[], options?: BuildOptions, tokenProgramId?: PublicKey): Promise<Transaction[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Build unsigned transactions requesting execution-time empty-account checks.
|
|
45
|
+
* Requires the upgraded program's selector 2. It never burns or falls back to
|
|
46
|
+
* burn-and-close. Ineligible targets can be skipped, including funded WSOL.
|
|
47
|
+
* Only a blockhash is fetched; the program enforces account eligibility.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildCloseEmptyTokenAccountsTransactions(connection: Connection, programId: PublicKey, user: PublicKey, pairs: Pair[], options?: BuildOptions, tokenProgramId?: PublicKey): Promise<Transaction[]>;
|
|
43
50
|
/**
|
|
44
51
|
* Build a single **TransactionInstruction** that calls the on-chain
|
|
45
52
|
* burn+close program for a specific Token Program (either SPL Token or Token-2022).
|
|
@@ -51,3 +58,11 @@ export declare function buildBurnAndCloseTransactions(connection: Connection, pr
|
|
|
51
58
|
* do not rely on Buffer-only methods on the returned data.
|
|
52
59
|
*/
|
|
53
60
|
export declare function buildBurnAndCloseInstruction(programId: PublicKey, user: PublicKey, pairs: Pair[], tokenProgramId: PublicKey): Promise<TransactionInstruction | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Build one close-empty instruction for SPL Token or Token-2022, without RPC.
|
|
63
|
+
* Returns null for empty input. Requires deployed selector 2 support; eligibility
|
|
64
|
+
* is checked by the program at execution, and ineligible targets may be skipped.
|
|
65
|
+
* This path never burns, including when tokens arrive after a preview.
|
|
66
|
+
* Data is a browser-safe Uint8Array typed as Buffer; avoid Buffer-only methods.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildCloseEmptyTokenAccountsInstruction(programId: PublicKey, user: PublicKey, pairs: Pair[], tokenProgramId: PublicKey): Promise<TransactionInstruction | null>;
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ export const TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9
|
|
|
7
7
|
export const TOKEN_2022_PROGRAM_ID = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
|
|
8
8
|
/** Legacy transaction wire limit, including the signature-count prefix and signature slots. */
|
|
9
9
|
const PACKET_DATA_SIZE = 1232;
|
|
10
|
-
function packetSizeError() {
|
|
11
|
-
return new Error(
|
|
10
|
+
function packetSizeError(operation) {
|
|
11
|
+
return new Error(`${operation}: transaction could not be serialized within the 1,232-byte limit. ` +
|
|
12
12
|
"Reduce chunkSize.");
|
|
13
13
|
}
|
|
14
14
|
/** web3.js reports oversized messages as "Transaction too large" or a buffer overrun. */
|
|
@@ -29,9 +29,22 @@ function isPacketSizeError(error) {
|
|
|
29
29
|
* @returns Array of *unsigned* Transaction objects
|
|
30
30
|
*/
|
|
31
31
|
export async function buildBurnAndCloseTransactions(connection, programId, user, pairs, options = {}, tokenProgramId = TOKEN_PROGRAM_ID) {
|
|
32
|
+
return buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, 0);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build unsigned transactions requesting execution-time empty-account checks.
|
|
36
|
+
* Requires the upgraded program's selector 2. It never burns or falls back to
|
|
37
|
+
* burn-and-close. Ineligible targets can be skipped, including funded WSOL.
|
|
38
|
+
* Only a blockhash is fetched; the program enforces account eligibility.
|
|
39
|
+
*/
|
|
40
|
+
export async function buildCloseEmptyTokenAccountsTransactions(connection, programId, user, pairs, options = {}, tokenProgramId = TOKEN_PROGRAM_ID) {
|
|
41
|
+
return buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, 2);
|
|
42
|
+
}
|
|
43
|
+
async function buildTokenAccountTransactions(connection, programId, user, pairs, options, tokenProgramId, selector) {
|
|
44
|
+
const operation = selector === 0 ? "Burn and close" : "Close empty token accounts";
|
|
32
45
|
const chunkSize = options.chunkSize ?? 12;
|
|
33
46
|
if (!Number.isFinite(chunkSize) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
34
|
-
throw new Error(
|
|
47
|
+
throw new Error(`${operation}: chunkSize must be a finite positive integer.`);
|
|
35
48
|
}
|
|
36
49
|
if (!pairs.length)
|
|
37
50
|
return [];
|
|
@@ -42,30 +55,14 @@ export async function buildBurnAndCloseTransactions(connection, programId, user,
|
|
|
42
55
|
for (let i = 0; i < pairs.length; i += chunkSize) {
|
|
43
56
|
chunks.push(pairs.slice(i, i + chunkSize));
|
|
44
57
|
}
|
|
45
|
-
// Instruction data: [selector=0]
|
|
46
58
|
// Use Uint8Array (browser-safe) and cast for web3.js' Buffer typing.
|
|
47
|
-
const data = new Uint8Array([
|
|
59
|
+
const data = new Uint8Array([selector]);
|
|
48
60
|
const { blockhash, lastValidBlockHeight } = options.recentBlockhash !== undefined
|
|
49
61
|
? { blockhash: options.recentBlockhash, lastValidBlockHeight: options.lastValidBlockHeight }
|
|
50
62
|
: await connection.getLatestBlockhash({ commitment });
|
|
51
63
|
const txs = [];
|
|
52
64
|
for (const chunk of chunks) {
|
|
53
|
-
const
|
|
54
|
-
// NOTE: Mark the user as signer in the instruction meta;
|
|
55
|
-
// the caller must actually sign the Transaction later.
|
|
56
|
-
{ pubkey: FEE_RECIPIENT, isSigner: false, isWritable: true },
|
|
57
|
-
{ pubkey: user, isSigner: true, isWritable: true },
|
|
58
|
-
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
|
|
59
|
-
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
60
|
-
];
|
|
61
|
-
for (const { mint, ata } of chunk) {
|
|
62
|
-
keys.push({ pubkey: mint, isSigner: false, isWritable: true }, { pubkey: ata, isSigner: false, isWritable: true });
|
|
63
|
-
}
|
|
64
|
-
const ix = new TransactionInstruction({
|
|
65
|
-
programId,
|
|
66
|
-
keys,
|
|
67
|
-
data,
|
|
68
|
-
});
|
|
65
|
+
const ix = tokenAccountInstruction(programId, user, chunk, tokenProgramId, data);
|
|
69
66
|
const tx = new Transaction().add(ix);
|
|
70
67
|
tx.feePayer = feePayer;
|
|
71
68
|
tx.recentBlockhash = blockhash;
|
|
@@ -76,13 +73,13 @@ export async function buildBurnAndCloseTransactions(connection, programId, user,
|
|
|
76
73
|
}
|
|
77
74
|
catch (error) {
|
|
78
75
|
if (isPacketSizeError(error)) {
|
|
79
|
-
throw packetSizeError();
|
|
76
|
+
throw packetSizeError(operation);
|
|
80
77
|
}
|
|
81
|
-
throw new Error(
|
|
78
|
+
throw new Error(`${operation}: transaction could not be serialized. ` +
|
|
82
79
|
"Check the supplied transaction inputs, including recentBlockhash and account keys.", { cause: error });
|
|
83
80
|
}
|
|
84
81
|
if (wireSize > PACKET_DATA_SIZE) {
|
|
85
|
-
throw packetSizeError();
|
|
82
|
+
throw packetSizeError(operation);
|
|
86
83
|
}
|
|
87
84
|
txs.push(tx);
|
|
88
85
|
}
|
|
@@ -102,8 +99,22 @@ export async function buildBurnAndCloseInstruction(programId, user, pairs, token
|
|
|
102
99
|
if (!pairs.length) {
|
|
103
100
|
return null;
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
return tokenAccountInstruction(programId, user, pairs, tokenProgramId, new Uint8Array([0]));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build one close-empty instruction for SPL Token or Token-2022, without RPC.
|
|
106
|
+
* Returns null for empty input. Requires deployed selector 2 support; eligibility
|
|
107
|
+
* is checked by the program at execution, and ineligible targets may be skipped.
|
|
108
|
+
* This path never burns, including when tokens arrive after a preview.
|
|
109
|
+
* Data is a browser-safe Uint8Array typed as Buffer; avoid Buffer-only methods.
|
|
110
|
+
*/
|
|
111
|
+
export async function buildCloseEmptyTokenAccountsInstruction(programId, user, pairs, tokenProgramId) {
|
|
112
|
+
if (!pairs.length) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return tokenAccountInstruction(programId, user, pairs, tokenProgramId, new Uint8Array([2]));
|
|
116
|
+
}
|
|
117
|
+
function tokenAccountInstruction(programId, user, pairs, tokenProgramId, data) {
|
|
107
118
|
// accounts order MUST match the on-chain program's expectation:
|
|
108
119
|
// [0] fee_recipient_account (your fee recipient)
|
|
109
120
|
// [1] user_wallet_account (the signer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unclaimedsol/spl-burn-close-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Build unsigned Solana transactions to burn SPL tokens, close token accounts, and reclaim rent with the Unclaimed SOL program.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Unclaimed SOL",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node ./scripts/build-cjs.js",
|
|
38
38
|
"prepack": "npm run build",
|
|
39
39
|
"test": "npm run build && node --test test/*.test.js",
|
|
40
|
-
"test:package": "node scripts/test-package.js"
|
|
40
|
+
"test:package": "node scripts/test-package.js",
|
|
41
|
+
"test:runtime": "npm run build && node scripts/test-runtime.js"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"@solana/web3.js": "^1.98.4"
|