@ar.io/sdk 4.0.0-solana.3 → 4.0.0-solana.5

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.
@@ -58,6 +58,7 @@ const antCommands_js_1 = require("./commands/antCommands.js");
58
58
  const arnsPurchaseCommands_js_1 = require("./commands/arnsPurchaseCommands.js");
59
59
  const escrowCommands_js_1 = require("./commands/escrowCommands.js");
60
60
  const gatewayWriteCommands_js_1 = require("./commands/gatewayWriteCommands.js");
61
+ const pruneCommands_js_1 = require("./commands/pruneCommands.js");
61
62
  const readCommands_js_1 = require("./commands/readCommands.js");
62
63
  const transfer_js_1 = require("./commands/transfer.js");
63
64
  const options_js_1 = require("./options.js");
@@ -491,6 +492,77 @@ const utils_js_1 = require("./utils.js");
491
492
  options: options_js_1.arnsPurchaseOptions,
492
493
  action: arnsPurchaseCommands_js_1.setPrimaryNameCLICommand,
493
494
  });
495
+ // # Prune / cleanup (Solana-only — permissionless crank surface)
496
+ (0, utils_js_1.makeCommand)({
497
+ name: 'prune-expired-names',
498
+ description: 'Batch-prune expired ArnsRecord PDAs (Solana-only). Discovers eligible records ' +
499
+ 'via getExpiredArnsRecords if --arns-records is omitted.',
500
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.max, options_js_1.optionMap.arnsRecords],
501
+ action: pruneCommands_js_1.pruneExpiredNamesCLICommand,
502
+ });
503
+ (0, utils_js_1.makeCommand)({
504
+ name: 'prune-name-to-returned',
505
+ description: 'Convert a single expired-but-not-yet-returned lease into a ReturnedName ' +
506
+ '(starts the Dutch auction). Solana-only.',
507
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.name],
508
+ action: pruneCommands_js_1.pruneNameToReturnedCLICommand,
509
+ });
510
+ (0, utils_js_1.makeCommand)({
511
+ name: 'prune-returned-names',
512
+ description: 'Batch-prune expired ReturnedName PDAs (Solana-only). Discovers via ' +
513
+ 'getExpiredReturnedNames if --returned-names is omitted.',
514
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.max, options_js_1.optionMap.returnedNames],
515
+ action: pruneCommands_js_1.pruneReturnedNamesCLICommand,
516
+ });
517
+ (0, utils_js_1.makeCommand)({
518
+ name: 'prune-expired-reservation',
519
+ description: 'Close an expired ReservedName PDA (Solana-only).',
520
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.name],
521
+ action: pruneCommands_js_1.pruneExpiredReservationCLICommand,
522
+ });
523
+ (0, utils_js_1.makeCommand)({
524
+ name: 'prune-gateway',
525
+ description: 'Slash + remove a deficient gateway (≥30 consecutive failures). Solana-only.',
526
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.gateway],
527
+ action: pruneCommands_js_1.pruneGatewayCLICommand,
528
+ });
529
+ (0, utils_js_1.makeCommand)({
530
+ name: 'finalize-gone',
531
+ description: 'GC a Leaving/Gone gateway whose leave window has fully elapsed. Solana-only.',
532
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.gateway],
533
+ action: pruneCommands_js_1.finalizeGoneCLICommand,
534
+ });
535
+ (0, utils_js_1.makeCommand)({
536
+ name: 'close-observation',
537
+ description: 'Reclaim rent from an Observation PDA whose epoch has been distributed. Solana-only.',
538
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.epochIndex, options_js_1.optionMap.observer],
539
+ action: pruneCommands_js_1.closeObservationCLICommand,
540
+ });
541
+ (0, utils_js_1.makeCommand)({
542
+ name: 'close-empty-delegation',
543
+ description: 'Close an empty Delegation PDA (amount == 0). Rent refunds to the original delegator. Solana-only.',
544
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.gateway, options_js_1.optionMap.delegator],
545
+ action: pruneCommands_js_1.closeEmptyDelegationCLICommand,
546
+ });
547
+ (0, utils_js_1.makeCommand)({
548
+ name: 'close-drained-withdrawal',
549
+ description: 'Close a drained Withdrawal PDA (amount == 0). Rent refunds to the original owner. Solana-only.',
550
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.owner, options_js_1.optionMap.withdrawalId],
551
+ action: pruneCommands_js_1.closeDrainedWithdrawalCLICommand,
552
+ });
553
+ (0, utils_js_1.makeCommand)({
554
+ name: 'release-vault',
555
+ description: 'Release tokens from an expired vault back to the owner (Solana-only). ' +
556
+ 'NOT permissionless — must be called from the vault owner wallet.',
557
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.vaultId, options_js_1.optionMap.owner],
558
+ action: pruneCommands_js_1.releaseVaultCLICommand,
559
+ });
560
+ (0, utils_js_1.makeCommand)({
561
+ name: 'close-expired-request',
562
+ description: 'Close an expired PrimaryNameRequest PDA. Solana-only.',
563
+ options: [...options_js_1.writeActionOptions, options_js_1.optionMap.initiator],
564
+ action: pruneCommands_js_1.closeExpiredRequestCLICommand,
565
+ });
494
566
  // # ANT Registry
495
567
  (0, utils_js_1.makeCommand)({
496
568
  name: 'get-ants-for-address',
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pruneExpiredNamesCLICommand = pruneExpiredNamesCLICommand;
4
+ exports.pruneNameToReturnedCLICommand = pruneNameToReturnedCLICommand;
5
+ exports.pruneReturnedNamesCLICommand = pruneReturnedNamesCLICommand;
6
+ exports.pruneExpiredReservationCLICommand = pruneExpiredReservationCLICommand;
7
+ exports.pruneGatewayCLICommand = pruneGatewayCLICommand;
8
+ exports.finalizeGoneCLICommand = finalizeGoneCLICommand;
9
+ exports.closeObservationCLICommand = closeObservationCLICommand;
10
+ exports.closeEmptyDelegationCLICommand = closeEmptyDelegationCLICommand;
11
+ exports.closeDrainedWithdrawalCLICommand = closeDrainedWithdrawalCLICommand;
12
+ exports.releaseVaultCLICommand = releaseVaultCLICommand;
13
+ exports.closeExpiredRequestCLICommand = closeExpiredRequestCLICommand;
14
+ const utils_js_1 = require("../utils.js");
15
+ function rejectAoBackend(o, command) {
16
+ if (o.ao) {
17
+ throw new Error(`${command} is Solana-only — Lua's tick() handles the equivalent on AO. Drop --ao.`);
18
+ }
19
+ }
20
+ async function getSolanaWriter(o) {
21
+ const { ario } = await (0, utils_js_1.writeARIOFromOptions)(o);
22
+ return ario;
23
+ }
24
+ function parseMaxNames(o) {
25
+ const raw = o.max;
26
+ if (raw === undefined) {
27
+ throw new Error('--max <count> is required (u8 batch size, 1-255)');
28
+ }
29
+ const n = Number(raw);
30
+ if (!Number.isInteger(n) || n < 1 || n > 255) {
31
+ throw new Error(`--max must be an integer 1-255 (got ${raw})`);
32
+ }
33
+ return n;
34
+ }
35
+ // =========================================
36
+ // ArNS prune
37
+ // =========================================
38
+ async function pruneExpiredNamesCLICommand(o) {
39
+ rejectAoBackend(o, 'prune-expired-names');
40
+ const max = parseMaxNames(o);
41
+ const ario = await getSolanaWriter(o);
42
+ // If `--arns-records` wasn't provided, discover them via the readable's
43
+ // helper. Cap at `max` so we never overshoot the ix's u8 batch parameter.
44
+ let records = o.arnsRecords ?? [];
45
+ if (records.length === 0) {
46
+ const now = Math.floor(Date.now() / 1000);
47
+ const expired = await ario.getExpiredArnsRecords(now);
48
+ records = expired.slice(0, max).map((r) => r.pubkey);
49
+ if (records.length === 0) {
50
+ return { message: 'No expired ArnsRecords to prune' };
51
+ }
52
+ }
53
+ else {
54
+ records = records.slice(0, max);
55
+ }
56
+ await (0, utils_js_1.assertConfirmationPrompt)(`Prune ${records.length} expired ArnsRecord(s)?`, o);
57
+ return ario.pruneExpiredNames({ maxNames: max, arnsRecords: records });
58
+ }
59
+ async function pruneNameToReturnedCLICommand(o) {
60
+ rejectAoBackend(o, 'prune-name-to-returned');
61
+ const name = (0, utils_js_1.requiredStringFromOptions)(o, 'name');
62
+ const ario = await getSolanaWriter(o);
63
+ await (0, utils_js_1.assertConfirmationPrompt)(`Convert expired lease for "${name}" to a ReturnedName (Dutch auction)?`, o);
64
+ return ario.pruneNameToReturned({ name });
65
+ }
66
+ async function pruneReturnedNamesCLICommand(o) {
67
+ rejectAoBackend(o, 'prune-returned-names');
68
+ const max = parseMaxNames(o);
69
+ const ario = await getSolanaWriter(o);
70
+ let returned = o.returnedNames ?? [];
71
+ if (returned.length === 0) {
72
+ const now = Math.floor(Date.now() / 1000);
73
+ const expired = await ario.getExpiredReturnedNames(now);
74
+ returned = expired.slice(0, max).map((r) => r.pubkey);
75
+ if (returned.length === 0) {
76
+ return { message: 'No expired ReturnedNames to prune' };
77
+ }
78
+ }
79
+ else {
80
+ returned = returned.slice(0, max);
81
+ }
82
+ await (0, utils_js_1.assertConfirmationPrompt)(`Prune ${returned.length} expired ReturnedName(s)?`, o);
83
+ return ario.pruneReturnedNames({
84
+ maxNames: max,
85
+ returnedNames: returned,
86
+ });
87
+ }
88
+ async function pruneExpiredReservationCLICommand(o) {
89
+ rejectAoBackend(o, 'prune-expired-reservation');
90
+ const name = (0, utils_js_1.requiredStringFromOptions)(o, 'name');
91
+ const ario = await getSolanaWriter(o);
92
+ await (0, utils_js_1.assertConfirmationPrompt)(`Close expired reservation for "${name}"?`, o);
93
+ return ario.pruneExpiredReservation({ name });
94
+ }
95
+ // =========================================
96
+ // Gateway prune
97
+ // =========================================
98
+ async function pruneGatewayCLICommand(o) {
99
+ rejectAoBackend(o, 'prune-gateway');
100
+ const gateway = (0, utils_js_1.requiredStringFromOptions)(o, 'gateway');
101
+ const ario = await getSolanaWriter(o);
102
+ await (0, utils_js_1.assertConfirmationPrompt)(`Slash + remove deficient gateway ${gateway}?`, o);
103
+ return ario.pruneGateway({ gateway });
104
+ }
105
+ async function finalizeGoneCLICommand(o) {
106
+ rejectAoBackend(o, 'finalize-gone');
107
+ const gateway = (0, utils_js_1.requiredStringFromOptions)(o, 'gateway');
108
+ const ario = await getSolanaWriter(o);
109
+ await (0, utils_js_1.assertConfirmationPrompt)(`Finalize-GC departed gateway ${gateway} (reclaim PDA rent)?`, o);
110
+ return ario.finalizeGone({ gateway });
111
+ }
112
+ // =========================================
113
+ // Rent reclaim
114
+ // =========================================
115
+ async function closeObservationCLICommand(o) {
116
+ rejectAoBackend(o, 'close-observation');
117
+ const epochIndexStr = (0, utils_js_1.requiredStringFromOptions)(o, 'epochIndex');
118
+ const observer = (0, utils_js_1.requiredStringFromOptions)(o, 'observer');
119
+ const epochIndex = Number(epochIndexStr);
120
+ if (!Number.isInteger(epochIndex) || epochIndex < 0) {
121
+ throw new Error(`--epoch-index must be a non-negative integer (got ${epochIndexStr})`);
122
+ }
123
+ const ario = await getSolanaWriter(o);
124
+ await (0, utils_js_1.assertConfirmationPrompt)(`Close Observation PDA (epoch ${epochIndex}, observer ${observer})?`, o);
125
+ return ario.closeObservation({ epochIndex, observer });
126
+ }
127
+ async function closeEmptyDelegationCLICommand(o) {
128
+ rejectAoBackend(o, 'close-empty-delegation');
129
+ const gateway = (0, utils_js_1.requiredStringFromOptions)(o, 'gateway');
130
+ const delegator = (0, utils_js_1.requiredStringFromOptions)(o, 'delegator');
131
+ const ario = await getSolanaWriter(o);
132
+ await (0, utils_js_1.assertConfirmationPrompt)(`Close empty Delegation PDA (gateway=${gateway}, delegator=${delegator})?`, o);
133
+ return ario.closeEmptyDelegation({ gateway, delegator });
134
+ }
135
+ async function closeDrainedWithdrawalCLICommand(o) {
136
+ rejectAoBackend(o, 'close-drained-withdrawal');
137
+ const owner = (0, utils_js_1.requiredStringFromOptions)(o, 'owner');
138
+ const withdrawalIdStr = (0, utils_js_1.requiredStringFromOptions)(o, 'withdrawalId');
139
+ // Validate before BigInt() — `BigInt('0xff')` and `BigInt(' 1 ')` succeed
140
+ // and `BigInt('abc')` throws an opaque SyntaxError. Restrict to the u64
141
+ // decimal form the on-chain seed encoder expects so the CLI fails with a
142
+ // clear message instead of a downstream parser error.
143
+ if (!/^\d+$/.test(withdrawalIdStr)) {
144
+ throw new Error(`--withdrawal-id must be a non-negative decimal integer (got "${withdrawalIdStr}")`);
145
+ }
146
+ const withdrawalId = BigInt(withdrawalIdStr);
147
+ const ario = await getSolanaWriter(o);
148
+ await (0, utils_js_1.assertConfirmationPrompt)(`Close drained Withdrawal PDA (owner=${owner}, id=${withdrawalIdStr})?`, o);
149
+ return ario.closeDrainedWithdrawal({ owner, withdrawalId });
150
+ }
151
+ // =========================================
152
+ // Vault + primary-name request
153
+ // =========================================
154
+ async function releaseVaultCLICommand(o) {
155
+ rejectAoBackend(o, 'release-vault');
156
+ // The on-chain handler requires `owner: Signer` — the SDK uses the
157
+ // configured signer as the owner. The `--owner` flag is accepted as
158
+ // documentation but ignored unless it matches the signer; fail loud if
159
+ // it doesn't, so users don't think they can release someone else's vault.
160
+ const ario = await getSolanaWriter(o);
161
+ if (o.owner) {
162
+ const signerAddr = (await (0, utils_js_1.writeARIOFromOptions)(o)).signerAddress;
163
+ if (o.owner !== signerAddr) {
164
+ throw new Error(`release-vault: --owner ${o.owner} does not match signer ${signerAddr}. ` +
165
+ `release_vault is owner-signed; use the owner's wallet to call it.`);
166
+ }
167
+ }
168
+ const vaultIdStr = (0, utils_js_1.requiredStringFromOptions)(o, 'vaultId');
169
+ const vaultId = vaultIdStr;
170
+ await (0, utils_js_1.assertConfirmationPrompt)(`Release expired vault id=${vaultIdStr} (transfer tokens back to owner)?`, o);
171
+ return ario.releaseVault({ vaultId });
172
+ }
173
+ async function closeExpiredRequestCLICommand(o) {
174
+ rejectAoBackend(o, 'close-expired-request');
175
+ const initiator = (0, utils_js_1.requiredStringFromOptions)(o, 'initiator');
176
+ const ario = await getSolanaWriter(o);
177
+ await (0, utils_js_1.assertConfirmationPrompt)(`Close expired primary-name request from ${initiator}?`, o);
178
+ return ario.closeExpiredRequest({ initiator });
179
+ }
@@ -424,6 +424,35 @@ exports.optionMap = {
424
424
  description: 'Reassign all affiliated names to the new process',
425
425
  type: 'boolean',
426
426
  },
427
+ // -----------------------------------------------------------------
428
+ // Prune / cleanup flags (Solana-only — see pruneCommands.ts)
429
+ // -----------------------------------------------------------------
430
+ gateway: {
431
+ alias: '--gateway <gateway>',
432
+ description: 'The gateway operator address (prune / finalize commands)',
433
+ },
434
+ delegator: {
435
+ alias: '--delegator <delegator>',
436
+ description: 'The delegator address (close-empty-delegation)',
437
+ },
438
+ observer: {
439
+ alias: '--observer <observer>',
440
+ description: 'The observer address (close-observation)',
441
+ },
442
+ max: {
443
+ alias: '--max <max>',
444
+ description: 'Per-tx batch size (1-255, u8) for prune-expired-names / prune-returned-names',
445
+ },
446
+ arnsRecords: {
447
+ alias: '--arns-records <arnsRecords...>',
448
+ description: 'Explicit ArnsRecord PDAs to prune. Default: discover via getExpiredArnsRecords.',
449
+ type: 'array',
450
+ },
451
+ returnedNames: {
452
+ alias: '--returned-names <returnedNames...>',
453
+ description: 'Explicit ReturnedName PDAs to prune. Default: discover via getExpiredReturnedNames.',
454
+ type: 'array',
455
+ },
427
456
  };
428
457
  exports.walletOptions = [
429
458
  exports.optionMap.walletFile,
@@ -330,6 +330,9 @@ function deserializeGateway(data) {
330
330
  const statusIdx = r.readU8(); // 0=Joined, 1=Leaving
331
331
  const startTimestamp = r.readI64AsNumber();
332
332
  const leaveTimestamp = r.readOptionI64();
333
+ // leave_epoch_duration: i64 — snapshot of epoch_settings.epoch_duration captured
334
+ // at leave_network/prune_gateway. Not surfaced on AoGateway; consume to stay aligned.
335
+ r.skip(8);
333
336
  // GatewayStats
334
337
  const passedEpochCount = r.readU32();
335
338
  const failedEpochCount = r.readU32();
@@ -338,17 +341,17 @@ function deserializeGateway(data) {
338
341
  const observedEpochCount = r.readU32();
339
342
  const failedConsecutiveEpochs = r.readU8();
340
343
  const passedConsecutiveEpochs = r.readU8();
341
- // GatewayWeights (6 x u64)
344
+ // GatewayWeights (7 x u64 — 7th is weights_epoch, set by tally_weights)
342
345
  const stakeWeight = r.readU64AsNumber();
343
346
  const tenureWeight = r.readU64AsNumber();
344
347
  const gatewayPerformanceRatio = r.readU64AsNumber();
345
348
  const observerPerformanceRatio = r.readU64AsNumber();
346
349
  const compositeWeight = r.readU64AsNumber();
347
350
  const normalizedCompositeWeight = r.readU64AsNumber();
348
- // GatewaySettings2
351
+ r.skip(8); // weights_epoch — not surfaced on AoGatewayWeights
352
+ // GatewaySettings2 (auto_stake removed in cfc7a8b2 — never existed on Solana)
349
353
  const allowDelegatedStaking = r.readBool();
350
354
  const delegateRewardShareRatio = r.readU16();
351
- const autoStake = r.readBool();
352
355
  const minDelegatedStake = r.readU64AsNumber();
353
356
  const allowlistEnabled = r.readBool();
354
357
  // RegistryIndex (index: u32, _reserved: u8 — was is_registered:bool)
@@ -386,7 +389,7 @@ function deserializeGateway(data) {
386
389
  delegateRewardShareRatio,
387
390
  allowedDelegates: [], // populated separately from allowlist PDAs
388
391
  minDelegatedStake,
389
- autoStake,
392
+ autoStake: false, // not an on-chain field on Solana; preserved on AoGatewaySettings for AO parity
390
393
  label,
391
394
  note,
392
395
  properties,
@@ -463,8 +463,12 @@ class TokenEscrow {
463
463
  saltLen: args.saltLen ?? 32,
464
464
  messageNonce: escrow.nonce,
465
465
  });
466
+ // The on-chain claim handler delivers liquid tokens to
467
+ // `claimantTokenAccount`; for fresh-wallet claimants the canonical ATA
468
+ // doesn't exist yet (#3012). Idempotent-create when canonical.
469
+ const createAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
466
470
  // RSA-PSS-4096 verification is CU-intensive; use 400K.
467
- return this.send([ix], 400_000);
471
+ return this.send(createAtaIx ? [createAtaIx, ix] : [ix], 400_000);
468
472
  }
469
473
  async claimTokensArweaveIx(args) {
470
474
  if (args.signature.length !== 512) {
@@ -500,7 +504,10 @@ class TokenEscrow {
500
504
  ...args,
501
505
  messageNonce: escrow.nonce,
502
506
  });
503
- return this.send([ix]);
507
+ // Same fresh-wallet #3012 vector as claimTokensArweave — bundle a
508
+ // canonical-ATA idempotent-create when applicable.
509
+ const createAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
510
+ return this.send(createAtaIx ? [createAtaIx, ix] : [ix]);
504
511
  }
505
512
  async claimTokensEthereumIx(args) {
506
513
  if (args.signature.length !== 65) {
@@ -604,16 +611,53 @@ class TokenEscrow {
604
611
  * (see the introspection function's tolerance), so any modest clock skew
605
612
  * between client and chain is absorbed.
606
613
  */
614
+ /**
615
+ * Idempotent-create the claimant's canonical ATA when needed.
616
+ *
617
+ * The on-chain claim handler delivers liquid tokens directly to
618
+ * `claimantTokenAccount` for expired vaults AND for token escrows.
619
+ * For active vaults, the `claim_vault_*` Anchor Accounts struct still
620
+ * declares `claimant_token_account: Account<TokenAccount>`, which forces
621
+ * Anchor's account-load-time validation to require the account exist
622
+ * even though the active path doesn't write to it. Either way: if the
623
+ * claimant is a fresh wallet that has never held this mint, the ATA
624
+ * doesn't exist and the tx fails with `AccountNotInitialized` (#3012).
625
+ *
626
+ * Returns `null` when the caller passed a non-canonical
627
+ * `claimantTokenAccount` (manually-created non-ATA token account,
628
+ * presumably already exists — caller's responsibility).
629
+ */
630
+ async _createClaimantAtaIfCanonical(claimant, claimantTokenAccount, mint) {
631
+ const canonical = await (0, ata_js_1.getAssociatedTokenAddressKit)(mint, claimant);
632
+ if (claimantTokenAccount !== canonical)
633
+ return null;
634
+ const signer = this.requireSigner('createClaimantAtaIfCanonical');
635
+ return buildCreateAtaIdempotentIx(signer.address, canonical, claimant, mint);
636
+ }
607
637
  async maybeBundleVaultedTransfer(escrow, args, claimIx) {
608
638
  const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
609
639
  const remaining = escrow.vaultEndTimestamp - nowSeconds;
610
640
  if (remaining <= 0n) {
611
- return [claimIx];
641
+ // Expired vault → claim handler delivers liquid to claimantTokenAccount.
642
+ // Idempotent-create that ATA if it's the canonical derivation so a
643
+ // first-time recipient just works.
644
+ const createClaimantAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
645
+ return createClaimantAtaIx ? [createClaimantAtaIx, claimIx] : [claimIx];
612
646
  }
613
647
  const signer = this.requireSigner('maybeBundleVaultedTransfer');
614
648
  const nextId = await this.getNextVaultId(args.claimant);
615
649
  const [vaultPda] = await (0, pda_js_1.getVaultPDA)(args.claimant, nextId, this.coreProgram);
616
650
  const vaultATA = await (0, ata_js_1.getAssociatedTokenAddressKit)(escrow.arioMint, vaultPda, true);
651
+ // Active-vault path: `claim_vault_*` still validates the claimant ATA
652
+ // at account-load-time (Anchor `Account<TokenAccount>` constraint),
653
+ // even though no liquid is written to it. Idempotent-create so a fresh
654
+ // claimant doesn't fail the ix with AccountNotInitialized (#3012).
655
+ const createClaimantAtaIx = await this._createClaimantAtaIfCanonical(args.claimant, args.claimantTokenAccount, escrow.arioMint);
656
+ // The new vault PDA's ATA must exist before `vaulted_transfer` reads it
657
+ // (else `AccountNotInitialized` #3012). Idempotent so a retry after a
658
+ // partial-failure tx is safe. Placed after the claim ix to preserve
659
+ // the "claim first" tx ordering invariant.
660
+ const createVaultAtaIx = buildCreateAtaIdempotentIx(signer.address, vaultATA, vaultPda, escrow.arioMint);
617
661
  const vaultedIx = await (0, vaultedTransfer_js_1.getVaultedTransferInstructionAsync)({
618
662
  vault: vaultPda,
619
663
  senderTokenAccount: args.payerTokenAccount,
@@ -624,7 +668,8 @@ class TokenEscrow {
624
668
  lockDurationSeconds: remaining,
625
669
  revocable: escrow.vaultRevocable,
626
670
  }, { programAddress: this.coreProgram });
627
- return [claimIx, vaultedIx];
671
+ const head = createClaimantAtaIx ? [createClaimantAtaIx] : [];
672
+ return [...head, claimIx, createVaultAtaIx, vaultedIx];
628
673
  }
629
674
  /** Read the recipient's `VaultCounter.nextId`, defaulting to 0n if the
630
675
  * counter PDA hasn't been initialised yet (first vault for that owner). */