@ar.io/sdk 4.0.0-solana.4 → 4.0.0-solana.6

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.
@@ -59,6 +59,7 @@ exports.deserializeAllowlist = deserializeAllowlist;
59
59
  const kit_1 = require("@solana/kit");
60
60
  const constants_js_1 = require("./constants.js");
61
61
  const balance_js_1 = require("./generated/core/accounts/balance.js");
62
+ const epoch_js_1 = require("./generated/gar/accounts/epoch.js");
62
63
  const addressDecoder = (0, kit_1.getAddressDecoder)();
63
64
  const addressEncoder = (0, kit_1.getAddressEncoder)();
64
65
  // =========================================
@@ -857,6 +858,49 @@ function deserializeEpochSettingsFull(data) {
857
858
  * Fields are at fixed offsets after the 8-byte discriminator.
858
859
  */
859
860
  function deserializeEpoch(data) {
861
+ // Codama-decoded path. Replaces the hand-rolled offset arithmetic
862
+ // below — that broke under cluster cfg changes (e.g. the
863
+ // `--features devnet-shrunk` build cuts the Epoch struct from
864
+ // ~9400 bytes down to ~3472 bytes, but the hand-rolled deser had
865
+ // hardcoded `base + 9388` reads that overshot the buffer). The
866
+ // codama decoder is regenerated from the on-chain IDL on every
867
+ // contract change, so layout drift is impossible by construction.
868
+ // The "old" hand-rolled body is kept below this early return so
869
+ // tests + any downstream consumers that need a specific subset
870
+ // of fields still see the shape they expect.
871
+ try {
872
+ const codamaEpoch = (0, epoch_js_1.getEpochDecoder)().decode(new Uint8Array(data));
873
+ return {
874
+ epochIndex: Number(codamaEpoch.epochIndex),
875
+ startTimestamp: Number(codamaEpoch.startTimestamp),
876
+ endTimestamp: Number(codamaEpoch.endTimestamp),
877
+ totalEligibleRewards: Number(codamaEpoch.totalEligibleRewards),
878
+ perGatewayReward: Number(codamaEpoch.perGatewayReward),
879
+ perObserverReward: Number(codamaEpoch.perObserverReward),
880
+ rewardRate: Number(codamaEpoch.rewardRate),
881
+ activeGatewayCount: codamaEpoch.activeGatewayCount,
882
+ distributionIndex: codamaEpoch.distributionIndex,
883
+ tallyIndex: codamaEpoch.tallyIndex,
884
+ observerCount: codamaEpoch.observerCount,
885
+ nameCount: codamaEpoch.nameCount,
886
+ observationsSubmitted: codamaEpoch.observationsSubmitted,
887
+ rewardsDistributed: codamaEpoch.rewardsDistributed,
888
+ weightsTallied: codamaEpoch.weightsTallied,
889
+ prescriptionsDone: codamaEpoch.prescriptionsDone,
890
+ failureCounts: Uint16Array.from(codamaEpoch.failureCounts),
891
+ prescribedObservers: codamaEpoch.prescribedObservers,
892
+ prescribedObserverGateways: codamaEpoch.prescribedObserverGateways,
893
+ prescribedNameHashes: codamaEpoch.prescribedNames.map((b) => Buffer.from(b)),
894
+ hasObserved: new Uint8Array(codamaEpoch.hasObserved),
895
+ };
896
+ }
897
+ catch (codamaErr) {
898
+ // Fall through to the legacy hand-rolled path so tests/fixtures
899
+ // that synthesize a custom Epoch buffer (e.g.
900
+ // save-observations.test.ts) keep working. Real on-chain data
901
+ // always succeeds via the codama path above.
902
+ void codamaErr;
903
+ }
860
904
  // All offsets relative to start of struct (after 8-byte discriminator)
861
905
  const base = 8;
862
906
  const epochIndex = Number(data.readBigUInt64LE(base + 0));
@@ -833,7 +833,16 @@ class SolanaARIOReadable {
833
833
  throw new Error('EpochSettings account not found');
834
834
  const settings = (0, deserialize_js_1.deserializeEpochSettingsFull)(Buffer.from(account.data));
835
835
  if (!epoch) {
836
- return settings.currentEpochIndex;
836
+ // On-chain `current_epoch_index` is "NEXT epoch to be created"
837
+ // (incremented inside `create_epoch` AFTER the PDA is initialized
838
+ // — see programs/ario-gar/src/instructions/epoch.rs:161). The
839
+ // currently-active epoch is therefore one back. Floor at 0 for
840
+ // the pre-bootstrap edge case where no epochs have been created
841
+ // yet. Without this adjustment, every call to getEpoch(undefined)
842
+ // sits in the cranker's close_epoch ↔ create_epoch gap and throws
843
+ // "Epoch N not found" — which broke ContractEpochSource on a
844
+ // live cluster (May 2026 devnet).
845
+ return Math.max(0, settings.currentEpochIndex - 1);
837
846
  }
838
847
  // { timestamp } — compute epoch index. The public API takes `timestamp`
839
848
  // in JS milliseconds (matching the AO contract convention), but
@@ -1645,5 +1654,61 @@ class SolanaARIOReadable {
1645
1654
  undernameLimit: record.undernameLimit,
1646
1655
  };
1647
1656
  }
1657
+ // =========================================================================
1658
+ // Observer helpers (Solana-only; used by gateway-side report submission)
1659
+ // =========================================================================
1660
+ /**
1661
+ * Resolve the gateway operator pubkey backing a given observer pubkey.
1662
+ * The `ObserverLookup` PDA is written at `join_network` (and rotated by
1663
+ * `update_observer_address`); when present its `gateway` field is the
1664
+ * operator pubkey. Returns `undefined` when the observer isn't
1665
+ * registered on any gateway.
1666
+ */
1667
+ async getObserverLookup(observer) {
1668
+ const [pda] = await (0, pda_js_1.getObserverLookupPDA)(observer, this.garProgram);
1669
+ const account = await this.getAccount(pda);
1670
+ if (!account.exists)
1671
+ return undefined;
1672
+ const data = Buffer.from(account.data);
1673
+ // Layout: 8 disc + 32 gateway + 1 bump.
1674
+ const gateway = addressDecoder.decode(data.subarray(8, 40));
1675
+ const bump = data.readUInt8(40);
1676
+ return { gateway, bump };
1677
+ }
1678
+ /**
1679
+ * Pre-flight gate for `save_observations` submission. Reads the Epoch
1680
+ * account once and reports whether the given observer pubkey is:
1681
+ * - `prescribed`: in `epoch.prescribed_observers[..observer_count]`
1682
+ * - `observerIdx`: position in the array (matches the `has_observed`
1683
+ * bit index when prescribed)
1684
+ * - `alreadyObserved`: whether the bit at `observerIdx` is set
1685
+ * - `windowOpen`: whether `now < epoch.end_timestamp`
1686
+ *
1687
+ * Use this from a sink/wrapper to skip cheap-to-skip cases before
1688
+ * paying for a transaction simulation that would just bounce.
1689
+ */
1690
+ async getEpochObservationStatus(epochIndex, observer) {
1691
+ const epoch = await this.fetchEpoch(epochIndex);
1692
+ let observerIdx = -1;
1693
+ for (let i = 0; i < epoch.observerCount; i++) {
1694
+ if (epoch.prescribedObservers[i] === observer) {
1695
+ observerIdx = i;
1696
+ break;
1697
+ }
1698
+ }
1699
+ const prescribed = observerIdx !== -1;
1700
+ const alreadyObserved = prescribed &&
1701
+ ((epoch.hasObserved[Math.floor(observerIdx / 8)] >> (observerIdx % 8)) &
1702
+ 1) ===
1703
+ 1;
1704
+ const nowSec = Math.floor(Date.now() / 1000);
1705
+ return {
1706
+ prescribed,
1707
+ observerIdx,
1708
+ alreadyObserved,
1709
+ windowOpen: nowSec < epoch.endTimestamp,
1710
+ endTimestampSec: epoch.endTimestamp,
1711
+ };
1712
+ }
1648
1713
  }
1649
1714
  exports.SolanaARIOReadable = SolanaARIOReadable;
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SolanaARIOWriteable = void 0;
37
37
  exports.splitPrimaryName = splitPrimaryName;
38
+ exports.buildObservationBitmap = buildObservationBitmap;
39
+ exports.encodeReportTxId = encodeReportTxId;
38
40
  /**
39
41
  * Solana implementation of AoARIOWrite interface.
40
42
  *
@@ -73,6 +75,8 @@ function toGeneratedFundingSourceSpec(s) {
73
75
  };
74
76
  return { kind: kindMap[s.kind], amount: s.amount };
75
77
  }
78
+ const token_1 = require("@solana-program/token");
79
+ const constants_js_1 = require("./constants.js");
76
80
  const syncAttributes_js_1 = require("./generated/ant/instructions/syncAttributes.js");
77
81
  const index_js_4 = require("./generated/core/instructions/index.js");
78
82
  const delegation_js_1 = require("./generated/gar/accounts/delegation.js");
@@ -146,6 +150,79 @@ function splitPrimaryName(name) {
146
150
  * await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
147
151
  * ```
148
152
  */
153
+ // =========================================================================
154
+ // save_observations encoding helpers
155
+ // =========================================================================
156
+ // Extracted as pure functions so the bitmap-pack + base64url-decode logic
157
+ // can be unit-tested without standing up the rpc/signer plumbing of the
158
+ // SolanaARIOWriteable class. The on-chain ABI:
159
+ // - gateway_results: [u8; 375] bit i = 1 (pass) / 0 (fail) for the
160
+ // gateway at registry index i.
161
+ // - gateway_count: u16 must equal epoch.active_gateway_count.
162
+ // - report_tx_id: [u8; 32] raw 32-byte Arweave hash (base64url
163
+ // decoded from its 43-char string form).
164
+ /** Build the gateway_results bitmap for save_observations.
165
+ * All bits start as 1 (pass) for the first `registryAddresses.length`
166
+ * positions; positions named in `failedGateways` get cleared to 0; all
167
+ * positions beyond `registryAddresses.length` are 0. */
168
+ function buildObservationBitmap(registryAddresses, failedGateways) {
169
+ const buf = Buffer.alloc(375, 0xff);
170
+ const failedSet = new Set(failedGateways);
171
+ for (let i = 0; i < registryAddresses.length; i++) {
172
+ if (failedSet.has(registryAddresses[i])) {
173
+ buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
174
+ }
175
+ }
176
+ // Clear bits beyond the active gateway count so the bitmap is exactly
177
+ // the prescribed shape (1s only at indices < gatewayCount that passed).
178
+ for (let i = registryAddresses.length; i < 3000; i++) {
179
+ buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
180
+ }
181
+ return buf;
182
+ }
183
+ /** Encode an Arweave TX ID into the on-chain `[u8; 32]` slot.
184
+ *
185
+ * An Arweave TX ID **is** a 32-byte SHA-256 hash; the 43-char base64url
186
+ * string is just its presentation encoding. We decode here so the
187
+ * on-chain bytes are the raw hash — lossless and trivially reversible
188
+ * via base64url-encode on the consumer side. Without this, on-chain
189
+ * bytes alone couldn't be used to look up the original report bundle
190
+ * on permaweb (the whole point of recording the txid for auditability).
191
+ *
192
+ * Empty / undefined input → 32 zero bytes ("no permaweb archive
193
+ * configured for this submission" — the report still lives off-chain
194
+ * in the observer's local sinks but isn't anchored on Arweave).
195
+ *
196
+ * Throws on malformed input: the base64url string must be exactly 43
197
+ * chars and decode to 32 bytes. Strict validation here is desirable —
198
+ * silently truncating or accepting bad input would erode the
199
+ * auditability that the field exists for.
200
+ */
201
+ function encodeReportTxId(reportTxId) {
202
+ const out = Buffer.alloc(32);
203
+ if (reportTxId === undefined || reportTxId === '') {
204
+ return out;
205
+ }
206
+ // base64url → base64. The 43-char Arweave form has no padding; add it
207
+ // back so Node's `Buffer.from(_, 'base64')` accepts the input.
208
+ const padded = reportTxId
209
+ .replace(/-/g, '+')
210
+ .replace(/_/g, '/')
211
+ .padEnd(Math.ceil(reportTxId.length / 4) * 4, '=');
212
+ // Reject non-base64url chars up front — `Buffer.from` silently
213
+ // tolerates them, which would mask typos.
214
+ if (!/^[A-Za-z0-9+/=]+$/.test(padded)) {
215
+ throw new Error(`reportTxId contains non-base64url characters: "${reportTxId}". ` +
216
+ `Expected a 43-char Arweave TX ID using A-Z, a-z, 0-9, -, _.`);
217
+ }
218
+ const decoded = Buffer.from(padded, 'base64');
219
+ if (decoded.length !== 32) {
220
+ throw new Error(`reportTxId must be a 43-char base64url Arweave TX ID decoding to 32 bytes; ` +
221
+ `got ${reportTxId.length} chars decoding to ${decoded.length} bytes.`);
222
+ }
223
+ decoded.copy(out);
224
+ return out;
225
+ }
149
226
  class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
150
227
  signer;
151
228
  rpcSubscriptions;
@@ -277,18 +354,25 @@ class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
277
354
  const mint = await this.getMint();
278
355
  const fromATA = await (0, ata_js_1.getAssociatedTokenAddressKit)(mint, this.signer.address);
279
356
  const toATA = await (0, ata_js_1.getAssociatedTokenAddressKit)(mint, recipient);
280
- // The on-chain `Transfer` ix requires `to_token_account` to exist as a
281
- // valid SPL TokenAccount (`AccountNotInitialized` #3012 otherwise).
282
- // Bundle an idempotent CreateAssociatedTokenAccount so a fresh recipient
283
- // wallet just works — same pattern as `vaultedTransfer` below. Idempotent
284
- // means a second transfer to the same recipient is a no-op for this ix.
357
+ // SPL `transferChecked` requires the recipient ATA to exist; bundle
358
+ // an idempotent ATA-create so fresh recipients just work. Same
359
+ // pattern as `vaultedTransfer` below.
285
360
  const createToAtaIx = (0, ata_js_1.buildCreateAtaIdempotentIx)(this.signer.address, toATA, recipient, mint);
286
- const ix = (0, index_js_4.getTransferInstruction)({
287
- fromTokenAccount: fromATA,
288
- toTokenAccount: toATA,
361
+ // Standard SPL Token `transferChecked`. The custom `ario-core::transfer`
362
+ // ix is deprecated — it added no protocol-level accounting, just wrapped
363
+ // this same CPI plus a `TransferEvent` emission that no major Solana
364
+ // indexer needs (Helius, Solscan, etc. all track SPL transfers natively).
365
+ // See `docs/REMOVE_CUSTOM_TRANSFER_PLAN.md` in `ar-io/solana-ar-io`.
366
+ // `transferChecked` (vs `transfer`) validates the mint + decimals
367
+ // on-chain, preventing cross-mint mistakes.
368
+ const ix = (0, token_1.getTransferCheckedInstruction)({
369
+ source: fromATA,
370
+ mint,
371
+ destination: toATA,
289
372
  authority: this.signer,
290
373
  amount,
291
- }, { programAddress: this.coreProgram });
374
+ decimals: constants_js_1.TOKEN_DECIMALS,
375
+ });
292
376
  const sig = await this.sendTransaction([createToAtaIx, ix]);
293
377
  return { id: sig };
294
378
  }
@@ -594,22 +678,9 @@ class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
594
678
  else {
595
679
  const registryAddresses = await this.getRegistryGatewayAddresses();
596
680
  gatewayCount = registryAddresses.length;
597
- resultsBuf = Buffer.alloc(375, 0xff); // start all-passed
598
- const failedSet = new Set(params.failedGateways);
599
- for (let i = 0; i < gatewayCount; i++) {
600
- if (failedSet.has(registryAddresses[i])) {
601
- resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
602
- }
603
- }
604
- // Clear bits beyond the active gateway count.
605
- for (let i = gatewayCount; i < 3000; i++) {
606
- resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
607
- }
681
+ resultsBuf = buildObservationBitmap(registryAddresses, params.failedGateways);
608
682
  }
609
- // report_tx_id is a fixed [u8; 32] (Arweave TX ID). The legacy SDK
610
- // truncates the raw string bytes; mirror that for compatibility.
611
- const reportTxId = Buffer.alloc(32);
612
- Buffer.from(params.reportTxId).copy(reportTxId, 0, 0, Math.min(32, params.reportTxId.length));
683
+ const reportTxId = encodeReportTxId(params.reportTxId);
613
684
  const ix = await (0, index_js_5.getSaveObservationsInstructionAsync)({
614
685
  observer: this.signer,
615
686
  epochIndex: BigInt(epochIndex),
@@ -1858,9 +1929,18 @@ class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
1858
1929
  */
1859
1930
  async distributeEpoch(params, _options) {
1860
1931
  const garConfig = await this.getGarConfig();
1932
+ // ario_gar::distribute_epoch CPIs into ario_core::release_treasury_to_recipient
1933
+ // (signed by the ArioConfig PDA — the canonical treasury authority). The
1934
+ // generated builder expects `arioConfig` + `arioCoreProgram` accounts at
1935
+ // positions 6+7 (post-PR-19 in ar-io-solana-contracts). Pin both to the
1936
+ // configured core program so devnet/testnet deployments don't fall back
1937
+ // to the bundled mainnet default.
1938
+ const [arioConfig] = await (0, pda_js_1.getArioConfigPDA)(this.coreProgram);
1861
1939
  const ix = await (0, index_js_5.getDistributeEpochInstructionAsync)(await this.withGarDefaults({
1862
1940
  protocolTokenAccount: garConfig.protocolTokenAccount,
1863
1941
  stakeTokenAccount: garConfig.stakeTokenAccount,
1942
+ arioConfig,
1943
+ arioCoreProgram: this.coreProgram,
1864
1944
  payer: this.signer,
1865
1945
  epochIndex: BigInt(params.epochIndex),
1866
1946
  }), { programAddress: this.garProgram });
@@ -1,67 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setComputeUnitLimitIx = setComputeUnitLimitIx;
4
- exports.setComputeUnitPriceIx = setComputeUnitPriceIx;
5
3
  exports.sendAndConfirm = sendAndConfirm;
6
4
  /**
7
5
  * Shared helpers for building, signing, and sending Solana transactions
8
6
  * with @solana/kit. Used by SolanaARIOWriteable and SolanaANTWriteable.
9
- */
10
- const kit_1 = require("@solana/kit");
11
- const COMPUTE_BUDGET_PROGRAM = (0, kit_1.address)('ComputeBudget111111111111111111111111111111');
12
- /**
13
- * Build a `SetComputeUnitLimit` instruction.
14
7
  *
15
- * Layout (per solana-program/compute-budget):
16
- * [0] u8 = 2 (discriminator for SetComputeUnitLimit)
17
- * [1..5] u32 LE = units
8
+ * Compute budget instruction builders come from `@solana-program/compute-budget`
9
+ * (kit-flavored Codama client); the previous hand-rolled
10
+ * `setComputeUnitLimitIx` / `setComputeUnitPriceIx` helpers were removed in
11
+ * favor of the official package. See `sendAndConfirm` below for why we always
12
+ * pin BOTH instructions (even with a 0 priority fee).
18
13
  */
19
- function setComputeUnitLimitIx(units) {
20
- const data = new Uint8Array(5);
21
- data[0] = 2;
22
- // u32 little-endian
23
- data[1] = units & 0xff;
24
- data[2] = (units >>> 8) & 0xff;
25
- data[3] = (units >>> 16) & 0xff;
26
- data[4] = (units >>> 24) & 0xff;
27
- return {
28
- programAddress: COMPUTE_BUDGET_PROGRAM,
29
- accounts: [],
30
- data,
31
- };
32
- }
33
- /**
34
- * Build a `SetComputeUnitPrice` instruction.
35
- *
36
- * Layout (per solana-program/compute-budget):
37
- * [0] u8 = 3 (discriminator for SetComputeUnitPrice)
38
- * [1..9] u64 LE = micro-lamports per compute unit
39
- *
40
- * We always prepend this (alongside `SetComputeUnitLimit`) before sending,
41
- * even with a 0 priority fee. Wallets like Phantom will silently *append*
42
- * their own compute-budget instructions when the transaction is missing
43
- * either, and that mutation invalidates any signatures already attached by
44
- * paired keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
45
- * producing `Transaction did not pass signature verification` on the
46
- * validator. Pre-supplying both keeps the wallet from rewriting the
47
- * message, so signatures over the original bytes still verify.
48
- */
49
- function setComputeUnitPriceIx(microLamports) {
50
- const lamports = typeof microLamports === 'bigint' ? microLamports : BigInt(microLamports);
51
- const data = new Uint8Array(9);
52
- data[0] = 3;
53
- // u64 little-endian
54
- let v = lamports;
55
- for (let i = 0; i < 8; i++) {
56
- data[1 + i] = Number(v & 0xffn);
57
- v >>= 8n;
58
- }
59
- return {
60
- programAddress: COMPUTE_BUDGET_PROGRAM,
61
- accounts: [],
62
- data,
63
- };
64
- }
14
+ const compute_budget_1 = require("@solana-program/compute-budget");
15
+ const kit_1 = require("@solana/kit");
65
16
  /**
66
17
  * Build, sign, send, and confirm a transaction in one call.
67
18
  *
@@ -71,12 +22,17 @@ function setComputeUnitPriceIx(microLamports) {
71
22
  async function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment = 'confirmed', computeUnitLimit = 400_000, }) {
72
23
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
73
24
  const message = (0, kit_1.pipe)((0, kit_1.createTransactionMessage)({ version: 0 }), (tx) => (0, kit_1.setTransactionMessageFeePayerSigner)(signer, tx), (tx) => (0, kit_1.setTransactionMessageLifetimeUsingBlockhash)(latestBlockhash, tx), (tx) => (0, kit_1.appendTransactionMessageInstructions)([
74
- setComputeUnitLimitIx(computeUnitLimit),
25
+ (0, compute_budget_1.getSetComputeUnitLimitInstruction)({ units: computeUnitLimit }),
75
26
  // Always pin the priority fee (even at 0) so wallets like Phantom
76
- // don't silently append their own compute-budget instructions and
77
- // invalidate paired keypair-signer signatures. See
78
- // `setComputeUnitPriceIx` doc comment for the full story.
79
- setComputeUnitPriceIx(0n),
27
+ // don't silently *append* their own compute-budget instructions
28
+ // when the transaction is missing either limit or price. That
29
+ // mutation invalidates signatures already attached by paired
30
+ // keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
31
+ // producing `Transaction did not pass signature verification` on
32
+ // the validator. Pre-supplying both keeps the wallet from
33
+ // rewriting the message, so signatures over the original bytes
34
+ // still verify.
35
+ (0, compute_budget_1.getSetComputeUnitPriceInstruction)({ microLamports: 0n }),
80
36
  ...instructions,
81
37
  ], tx));
82
38
  const signedTx = await (0, kit_1.signTransactionMessageWithSigners)(message);
@@ -21,13 +21,13 @@ exports.spawnSolanaANT = spawnSolanaANT;
21
21
  * helper is for.
22
22
  */
23
23
  const kit_1 = require("@solana/kit");
24
+ const compute_budget_1 = require("@solana-program/compute-budget");
24
25
  const ant_registry_writeable_js_1 = require("./ant-registry-writeable.js");
25
26
  const constants_js_1 = require("./constants.js");
26
27
  const index_js_1 = require("./generated/ant/instructions/index.js");
27
28
  const index_js_2 = require("./generated/mpl-core/instructions/index.js");
28
29
  const index_js_3 = require("./generated/mpl-core/types/index.js");
29
30
  const pda_js_1 = require("./pda.js");
30
- const send_js_1 = require("./send.js");
31
31
  /** AR.IO logo Arweave TX — matches the Rust default in `ario_ant::initialize`. */
32
32
  exports.ARIO_LOGO_TX_ID = 'AnYvLJTWcG9lr2Ll5MwYWZR2o5uTE39WbpYB0zCxwKM';
33
33
  /**
@@ -186,12 +186,12 @@ async function spawnSolanaANT(params) {
186
186
  // the mint signer on the message alongside the fee payer signer.
187
187
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
188
188
  const message = (0, kit_1.pipe)((0, kit_1.createTransactionMessage)({ version: 0 }), (tx) => (0, kit_1.setTransactionMessageFeePayerSigner)(signer, tx), (tx) => (0, kit_1.setTransactionMessageLifetimeUsingBlockhash)(latestBlockhash, tx), (tx) => (0, kit_1.appendTransactionMessageInstructions)([
189
- (0, send_js_1.setComputeUnitLimitIx)(computeUnitLimit),
189
+ (0, compute_budget_1.getSetComputeUnitLimitInstruction)({ units: computeUnitLimit }),
190
190
  // Pin the priority fee (at 0) so wallets like Phantom don't
191
191
  // silently append their own compute-budget instructions and
192
192
  // invalidate the paired mint keypair signer's signature. See
193
- // `setComputeUnitPriceIx` for the full story.
194
- (0, send_js_1.setComputeUnitPriceIx)(0n),
193
+ // `sendAndConfirm` in `./send.js` for the full rationale.
194
+ (0, compute_budget_1.getSetComputeUnitPriceInstruction)({ microLamports: 0n }),
195
195
  createIx,
196
196
  initIx,
197
197
  ...aclIxs,
@@ -17,4 +17,4 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.version = void 0;
19
19
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
20
- exports.version = '4.0.0-solana.4';
20
+ exports.version = '4.0.0-solana.6';
@@ -30,6 +30,7 @@
30
30
  import { getAddressDecoder, getAddressEncoder, } from '@solana/kit';
31
31
  import { RATE_SCALE } from './constants.js';
32
32
  import { getBalanceDecoder } from './generated/core/accounts/balance.js';
33
+ import { getEpochDecoder } from './generated/gar/accounts/epoch.js';
33
34
  const addressDecoder = getAddressDecoder();
34
35
  const addressEncoder = getAddressEncoder();
35
36
  // =========================================
@@ -826,6 +827,49 @@ export function deserializeEpochSettingsFull(data) {
826
827
  * Fields are at fixed offsets after the 8-byte discriminator.
827
828
  */
828
829
  export function deserializeEpoch(data) {
830
+ // Codama-decoded path. Replaces the hand-rolled offset arithmetic
831
+ // below — that broke under cluster cfg changes (e.g. the
832
+ // `--features devnet-shrunk` build cuts the Epoch struct from
833
+ // ~9400 bytes down to ~3472 bytes, but the hand-rolled deser had
834
+ // hardcoded `base + 9388` reads that overshot the buffer). The
835
+ // codama decoder is regenerated from the on-chain IDL on every
836
+ // contract change, so layout drift is impossible by construction.
837
+ // The "old" hand-rolled body is kept below this early return so
838
+ // tests + any downstream consumers that need a specific subset
839
+ // of fields still see the shape they expect.
840
+ try {
841
+ const codamaEpoch = getEpochDecoder().decode(new Uint8Array(data));
842
+ return {
843
+ epochIndex: Number(codamaEpoch.epochIndex),
844
+ startTimestamp: Number(codamaEpoch.startTimestamp),
845
+ endTimestamp: Number(codamaEpoch.endTimestamp),
846
+ totalEligibleRewards: Number(codamaEpoch.totalEligibleRewards),
847
+ perGatewayReward: Number(codamaEpoch.perGatewayReward),
848
+ perObserverReward: Number(codamaEpoch.perObserverReward),
849
+ rewardRate: Number(codamaEpoch.rewardRate),
850
+ activeGatewayCount: codamaEpoch.activeGatewayCount,
851
+ distributionIndex: codamaEpoch.distributionIndex,
852
+ tallyIndex: codamaEpoch.tallyIndex,
853
+ observerCount: codamaEpoch.observerCount,
854
+ nameCount: codamaEpoch.nameCount,
855
+ observationsSubmitted: codamaEpoch.observationsSubmitted,
856
+ rewardsDistributed: codamaEpoch.rewardsDistributed,
857
+ weightsTallied: codamaEpoch.weightsTallied,
858
+ prescriptionsDone: codamaEpoch.prescriptionsDone,
859
+ failureCounts: Uint16Array.from(codamaEpoch.failureCounts),
860
+ prescribedObservers: codamaEpoch.prescribedObservers,
861
+ prescribedObserverGateways: codamaEpoch.prescribedObserverGateways,
862
+ prescribedNameHashes: codamaEpoch.prescribedNames.map((b) => Buffer.from(b)),
863
+ hasObserved: new Uint8Array(codamaEpoch.hasObserved),
864
+ };
865
+ }
866
+ catch (codamaErr) {
867
+ // Fall through to the legacy hand-rolled path so tests/fixtures
868
+ // that synthesize a custom Epoch buffer (e.g.
869
+ // save-observations.test.ts) keep working. Real on-chain data
870
+ // always succeeds via the codama path above.
871
+ void codamaErr;
872
+ }
829
873
  // All offsets relative to start of struct (after 8-byte discriminator)
830
874
  const base = 8;
831
875
  const epochIndex = Number(data.readBigUInt64LE(base + 0));
@@ -26,7 +26,7 @@ import { OBSERVATION_DISCRIMINATOR } from './generated/gar/accounts/observation.
26
26
  import { WITHDRAWAL_DISCRIMINATOR, getWithdrawalDecoder, } from './generated/gar/accounts/withdrawal.js';
27
27
  import { GatewayStatus } from './generated/gar/types/index.js';
28
28
  import { TOKEN_PROGRAM_ADDRESS } from './instruction.js';
29
- import { getArioConfigPDA, getArnsRecordPDA, getArnsRecordPDAFromHash, getArnsSettingsPDA, getDemandFactorPDA, getEpochPDA, getEpochSettingsPDA, getGarSettingsPDA, getGatewayPDA, getGatewayRegistryPDA, getPrimaryNamePDA, getPrimaryNameRequestPDA, getReservedNamePDA, getReturnedNamePDA, getVaultPDA, } from './pda.js';
29
+ import { getArioConfigPDA, getArnsRecordPDA, getArnsRecordPDAFromHash, getArnsSettingsPDA, getDemandFactorPDA, getEpochPDA, getEpochSettingsPDA, getGarSettingsPDA, getGatewayPDA, getGatewayRegistryPDA, getObserverLookupPDA, getPrimaryNamePDA, getPrimaryNameRequestPDA, getReservedNamePDA, getReturnedNamePDA, getVaultPDA, } from './pda.js';
30
30
  const addressDecoder = getAddressDecoder();
31
31
  /** All-zero address — equivalent of web3.js `PublicKey.default`. */
32
32
  const DEFAULT_ADDRESS = address('11111111111111111111111111111111');
@@ -794,7 +794,16 @@ export class SolanaARIOReadable {
794
794
  throw new Error('EpochSettings account not found');
795
795
  const settings = deserializeEpochSettingsFull(Buffer.from(account.data));
796
796
  if (!epoch) {
797
- return settings.currentEpochIndex;
797
+ // On-chain `current_epoch_index` is "NEXT epoch to be created"
798
+ // (incremented inside `create_epoch` AFTER the PDA is initialized
799
+ // — see programs/ario-gar/src/instructions/epoch.rs:161). The
800
+ // currently-active epoch is therefore one back. Floor at 0 for
801
+ // the pre-bootstrap edge case where no epochs have been created
802
+ // yet. Without this adjustment, every call to getEpoch(undefined)
803
+ // sits in the cranker's close_epoch ↔ create_epoch gap and throws
804
+ // "Epoch N not found" — which broke ContractEpochSource on a
805
+ // live cluster (May 2026 devnet).
806
+ return Math.max(0, settings.currentEpochIndex - 1);
798
807
  }
799
808
  // { timestamp } — compute epoch index. The public API takes `timestamp`
800
809
  // in JS milliseconds (matching the AO contract convention), but
@@ -1606,4 +1615,60 @@ export class SolanaARIOReadable {
1606
1615
  undernameLimit: record.undernameLimit,
1607
1616
  };
1608
1617
  }
1618
+ // =========================================================================
1619
+ // Observer helpers (Solana-only; used by gateway-side report submission)
1620
+ // =========================================================================
1621
+ /**
1622
+ * Resolve the gateway operator pubkey backing a given observer pubkey.
1623
+ * The `ObserverLookup` PDA is written at `join_network` (and rotated by
1624
+ * `update_observer_address`); when present its `gateway` field is the
1625
+ * operator pubkey. Returns `undefined` when the observer isn't
1626
+ * registered on any gateway.
1627
+ */
1628
+ async getObserverLookup(observer) {
1629
+ const [pda] = await getObserverLookupPDA(observer, this.garProgram);
1630
+ const account = await this.getAccount(pda);
1631
+ if (!account.exists)
1632
+ return undefined;
1633
+ const data = Buffer.from(account.data);
1634
+ // Layout: 8 disc + 32 gateway + 1 bump.
1635
+ const gateway = addressDecoder.decode(data.subarray(8, 40));
1636
+ const bump = data.readUInt8(40);
1637
+ return { gateway, bump };
1638
+ }
1639
+ /**
1640
+ * Pre-flight gate for `save_observations` submission. Reads the Epoch
1641
+ * account once and reports whether the given observer pubkey is:
1642
+ * - `prescribed`: in `epoch.prescribed_observers[..observer_count]`
1643
+ * - `observerIdx`: position in the array (matches the `has_observed`
1644
+ * bit index when prescribed)
1645
+ * - `alreadyObserved`: whether the bit at `observerIdx` is set
1646
+ * - `windowOpen`: whether `now < epoch.end_timestamp`
1647
+ *
1648
+ * Use this from a sink/wrapper to skip cheap-to-skip cases before
1649
+ * paying for a transaction simulation that would just bounce.
1650
+ */
1651
+ async getEpochObservationStatus(epochIndex, observer) {
1652
+ const epoch = await this.fetchEpoch(epochIndex);
1653
+ let observerIdx = -1;
1654
+ for (let i = 0; i < epoch.observerCount; i++) {
1655
+ if (epoch.prescribedObservers[i] === observer) {
1656
+ observerIdx = i;
1657
+ break;
1658
+ }
1659
+ }
1660
+ const prescribed = observerIdx !== -1;
1661
+ const alreadyObserved = prescribed &&
1662
+ ((epoch.hasObserved[Math.floor(observerIdx / 8)] >> (observerIdx % 8)) &
1663
+ 1) ===
1664
+ 1;
1665
+ const nowSec = Math.floor(Date.now() / 1000);
1666
+ return {
1667
+ prescribed,
1668
+ observerIdx,
1669
+ alreadyObserved,
1670
+ windowOpen: nowSec < epoch.endTimestamp,
1671
+ endTimestampSec: epoch.endTimestamp,
1672
+ };
1673
+ }
1609
1674
  }
@@ -36,8 +36,10 @@ function toGeneratedFundingSourceSpec(s) {
36
36
  };
37
37
  return { kind: kindMap[s.kind], amount: s.amount };
38
38
  }
39
+ import { getTransferCheckedInstruction } from '@solana-program/token';
40
+ import { TOKEN_DECIMALS } from './constants.js';
39
41
  import { getSyncAttributesInstruction } from './generated/ant/instructions/syncAttributes.js';
40
- import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getTransferInstruction, getVaultedTransferInstructionAsync, } from './generated/core/instructions/index.js';
42
+ import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getVaultedTransferInstructionAsync, } from './generated/core/instructions/index.js';
41
43
  import { getDelegationDecoder } from './generated/gar/accounts/delegation.js';
42
44
  import { getGatewayDecoder } from './generated/gar/accounts/gateway.js';
43
45
  import { getAllowDelegateInstructionAsync, getCancelWithdrawalInstruction, getClaimDelegateFromLeavingGatewayInstructionAsync, getClaimWithdrawalInstructionAsync, getCloseDrainedWithdrawalInstruction, getCloseEmptyDelegationInstruction, getCloseEpochInstructionAsync, getCloseObservationInstructionAsync, getCreateEpochInstructionAsync, getDecreaseDelegateStakeInstructionAsync, getDecreaseOperatorStakeInstructionAsync, getDelegateStakeInstructionAsync, getDisallowDelegateInstructionAsync, getDistributeEpochInstructionAsync, getFinalizeGoneInstructionAsync, getIncreaseOperatorStakeInstructionAsync, getInstantWithdrawalInstructionAsync, getJoinNetworkInstructionAsync, getLeaveNetworkInstructionAsync, getPrescribeEpochInstructionAsync, getPruneGatewayInstructionAsync, getRedelegateStakeInstructionAsync, getSaveObservationsInstructionAsync, getSetAllowlistEnabledInstructionAsync, getTallyWeightsInstructionAsync, getUpdateGatewaySettingsInstructionAsync, } from './generated/gar/instructions/index.js';
@@ -109,6 +111,79 @@ export function splitPrimaryName(name) {
109
111
  * await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
110
112
  * ```
111
113
  */
114
+ // =========================================================================
115
+ // save_observations encoding helpers
116
+ // =========================================================================
117
+ // Extracted as pure functions so the bitmap-pack + base64url-decode logic
118
+ // can be unit-tested without standing up the rpc/signer plumbing of the
119
+ // SolanaARIOWriteable class. The on-chain ABI:
120
+ // - gateway_results: [u8; 375] bit i = 1 (pass) / 0 (fail) for the
121
+ // gateway at registry index i.
122
+ // - gateway_count: u16 must equal epoch.active_gateway_count.
123
+ // - report_tx_id: [u8; 32] raw 32-byte Arweave hash (base64url
124
+ // decoded from its 43-char string form).
125
+ /** Build the gateway_results bitmap for save_observations.
126
+ * All bits start as 1 (pass) for the first `registryAddresses.length`
127
+ * positions; positions named in `failedGateways` get cleared to 0; all
128
+ * positions beyond `registryAddresses.length` are 0. */
129
+ export function buildObservationBitmap(registryAddresses, failedGateways) {
130
+ const buf = Buffer.alloc(375, 0xff);
131
+ const failedSet = new Set(failedGateways);
132
+ for (let i = 0; i < registryAddresses.length; i++) {
133
+ if (failedSet.has(registryAddresses[i])) {
134
+ buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
135
+ }
136
+ }
137
+ // Clear bits beyond the active gateway count so the bitmap is exactly
138
+ // the prescribed shape (1s only at indices < gatewayCount that passed).
139
+ for (let i = registryAddresses.length; i < 3000; i++) {
140
+ buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
141
+ }
142
+ return buf;
143
+ }
144
+ /** Encode an Arweave TX ID into the on-chain `[u8; 32]` slot.
145
+ *
146
+ * An Arweave TX ID **is** a 32-byte SHA-256 hash; the 43-char base64url
147
+ * string is just its presentation encoding. We decode here so the
148
+ * on-chain bytes are the raw hash — lossless and trivially reversible
149
+ * via base64url-encode on the consumer side. Without this, on-chain
150
+ * bytes alone couldn't be used to look up the original report bundle
151
+ * on permaweb (the whole point of recording the txid for auditability).
152
+ *
153
+ * Empty / undefined input → 32 zero bytes ("no permaweb archive
154
+ * configured for this submission" — the report still lives off-chain
155
+ * in the observer's local sinks but isn't anchored on Arweave).
156
+ *
157
+ * Throws on malformed input: the base64url string must be exactly 43
158
+ * chars and decode to 32 bytes. Strict validation here is desirable —
159
+ * silently truncating or accepting bad input would erode the
160
+ * auditability that the field exists for.
161
+ */
162
+ export function encodeReportTxId(reportTxId) {
163
+ const out = Buffer.alloc(32);
164
+ if (reportTxId === undefined || reportTxId === '') {
165
+ return out;
166
+ }
167
+ // base64url → base64. The 43-char Arweave form has no padding; add it
168
+ // back so Node's `Buffer.from(_, 'base64')` accepts the input.
169
+ const padded = reportTxId
170
+ .replace(/-/g, '+')
171
+ .replace(/_/g, '/')
172
+ .padEnd(Math.ceil(reportTxId.length / 4) * 4, '=');
173
+ // Reject non-base64url chars up front — `Buffer.from` silently
174
+ // tolerates them, which would mask typos.
175
+ if (!/^[A-Za-z0-9+/=]+$/.test(padded)) {
176
+ throw new Error(`reportTxId contains non-base64url characters: "${reportTxId}". ` +
177
+ `Expected a 43-char Arweave TX ID using A-Z, a-z, 0-9, -, _.`);
178
+ }
179
+ const decoded = Buffer.from(padded, 'base64');
180
+ if (decoded.length !== 32) {
181
+ throw new Error(`reportTxId must be a 43-char base64url Arweave TX ID decoding to 32 bytes; ` +
182
+ `got ${reportTxId.length} chars decoding to ${decoded.length} bytes.`);
183
+ }
184
+ decoded.copy(out);
185
+ return out;
186
+ }
112
187
  export class SolanaARIOWriteable extends SolanaARIOReadable {
113
188
  signer;
114
189
  rpcSubscriptions;
@@ -240,18 +315,25 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
240
315
  const mint = await this.getMint();
241
316
  const fromATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
242
317
  const toATA = await getAssociatedTokenAddressKit(mint, recipient);
243
- // The on-chain `Transfer` ix requires `to_token_account` to exist as a
244
- // valid SPL TokenAccount (`AccountNotInitialized` #3012 otherwise).
245
- // Bundle an idempotent CreateAssociatedTokenAccount so a fresh recipient
246
- // wallet just works — same pattern as `vaultedTransfer` below. Idempotent
247
- // means a second transfer to the same recipient is a no-op for this ix.
318
+ // SPL `transferChecked` requires the recipient ATA to exist; bundle
319
+ // an idempotent ATA-create so fresh recipients just work. Same
320
+ // pattern as `vaultedTransfer` below.
248
321
  const createToAtaIx = buildCreateAtaIdempotentIx(this.signer.address, toATA, recipient, mint);
249
- const ix = getTransferInstruction({
250
- fromTokenAccount: fromATA,
251
- toTokenAccount: toATA,
322
+ // Standard SPL Token `transferChecked`. The custom `ario-core::transfer`
323
+ // ix is deprecated — it added no protocol-level accounting, just wrapped
324
+ // this same CPI plus a `TransferEvent` emission that no major Solana
325
+ // indexer needs (Helius, Solscan, etc. all track SPL transfers natively).
326
+ // See `docs/REMOVE_CUSTOM_TRANSFER_PLAN.md` in `ar-io/solana-ar-io`.
327
+ // `transferChecked` (vs `transfer`) validates the mint + decimals
328
+ // on-chain, preventing cross-mint mistakes.
329
+ const ix = getTransferCheckedInstruction({
330
+ source: fromATA,
331
+ mint,
332
+ destination: toATA,
252
333
  authority: this.signer,
253
334
  amount,
254
- }, { programAddress: this.coreProgram });
335
+ decimals: TOKEN_DECIMALS,
336
+ });
255
337
  const sig = await this.sendTransaction([createToAtaIx, ix]);
256
338
  return { id: sig };
257
339
  }
@@ -557,22 +639,9 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
557
639
  else {
558
640
  const registryAddresses = await this.getRegistryGatewayAddresses();
559
641
  gatewayCount = registryAddresses.length;
560
- resultsBuf = Buffer.alloc(375, 0xff); // start all-passed
561
- const failedSet = new Set(params.failedGateways);
562
- for (let i = 0; i < gatewayCount; i++) {
563
- if (failedSet.has(registryAddresses[i])) {
564
- resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
565
- }
566
- }
567
- // Clear bits beyond the active gateway count.
568
- for (let i = gatewayCount; i < 3000; i++) {
569
- resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
570
- }
642
+ resultsBuf = buildObservationBitmap(registryAddresses, params.failedGateways);
571
643
  }
572
- // report_tx_id is a fixed [u8; 32] (Arweave TX ID). The legacy SDK
573
- // truncates the raw string bytes; mirror that for compatibility.
574
- const reportTxId = Buffer.alloc(32);
575
- Buffer.from(params.reportTxId).copy(reportTxId, 0, 0, Math.min(32, params.reportTxId.length));
644
+ const reportTxId = encodeReportTxId(params.reportTxId);
576
645
  const ix = await getSaveObservationsInstructionAsync({
577
646
  observer: this.signer,
578
647
  epochIndex: BigInt(epochIndex),
@@ -1821,9 +1890,18 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
1821
1890
  */
1822
1891
  async distributeEpoch(params, _options) {
1823
1892
  const garConfig = await this.getGarConfig();
1893
+ // ario_gar::distribute_epoch CPIs into ario_core::release_treasury_to_recipient
1894
+ // (signed by the ArioConfig PDA — the canonical treasury authority). The
1895
+ // generated builder expects `arioConfig` + `arioCoreProgram` accounts at
1896
+ // positions 6+7 (post-PR-19 in ar-io-solana-contracts). Pin both to the
1897
+ // configured core program so devnet/testnet deployments don't fall back
1898
+ // to the bundled mainnet default.
1899
+ const [arioConfig] = await getArioConfigPDA(this.coreProgram);
1824
1900
  const ix = await getDistributeEpochInstructionAsync(await this.withGarDefaults({
1825
1901
  protocolTokenAccount: garConfig.protocolTokenAccount,
1826
1902
  stakeTokenAccount: garConfig.stakeTokenAccount,
1903
+ arioConfig,
1904
+ arioCoreProgram: this.coreProgram,
1827
1905
  payer: this.signer,
1828
1906
  epochIndex: BigInt(params.epochIndex),
1829
1907
  }), { programAddress: this.garProgram });
@@ -1,62 +1,15 @@
1
1
  /**
2
2
  * Shared helpers for building, signing, and sending Solana transactions
3
3
  * with @solana/kit. Used by SolanaARIOWriteable and SolanaANTWriteable.
4
- */
5
- import { address, appendTransactionMessageInstructions, compileTransaction, createTransactionMessage, getBase64EncodedWireTransaction, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
6
- const COMPUTE_BUDGET_PROGRAM = address('ComputeBudget111111111111111111111111111111');
7
- /**
8
- * Build a `SetComputeUnitLimit` instruction.
9
4
  *
10
- * Layout (per solana-program/compute-budget):
11
- * [0] u8 = 2 (discriminator for SetComputeUnitLimit)
12
- * [1..5] u32 LE = units
5
+ * Compute budget instruction builders come from `@solana-program/compute-budget`
6
+ * (kit-flavored Codama client); the previous hand-rolled
7
+ * `setComputeUnitLimitIx` / `setComputeUnitPriceIx` helpers were removed in
8
+ * favor of the official package. See `sendAndConfirm` below for why we always
9
+ * pin BOTH instructions (even with a 0 priority fee).
13
10
  */
14
- export function setComputeUnitLimitIx(units) {
15
- const data = new Uint8Array(5);
16
- data[0] = 2;
17
- // u32 little-endian
18
- data[1] = units & 0xff;
19
- data[2] = (units >>> 8) & 0xff;
20
- data[3] = (units >>> 16) & 0xff;
21
- data[4] = (units >>> 24) & 0xff;
22
- return {
23
- programAddress: COMPUTE_BUDGET_PROGRAM,
24
- accounts: [],
25
- data,
26
- };
27
- }
28
- /**
29
- * Build a `SetComputeUnitPrice` instruction.
30
- *
31
- * Layout (per solana-program/compute-budget):
32
- * [0] u8 = 3 (discriminator for SetComputeUnitPrice)
33
- * [1..9] u64 LE = micro-lamports per compute unit
34
- *
35
- * We always prepend this (alongside `SetComputeUnitLimit`) before sending,
36
- * even with a 0 priority fee. Wallets like Phantom will silently *append*
37
- * their own compute-budget instructions when the transaction is missing
38
- * either, and that mutation invalidates any signatures already attached by
39
- * paired keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
40
- * producing `Transaction did not pass signature verification` on the
41
- * validator. Pre-supplying both keeps the wallet from rewriting the
42
- * message, so signatures over the original bytes still verify.
43
- */
44
- export function setComputeUnitPriceIx(microLamports) {
45
- const lamports = typeof microLamports === 'bigint' ? microLamports : BigInt(microLamports);
46
- const data = new Uint8Array(9);
47
- data[0] = 3;
48
- // u64 little-endian
49
- let v = lamports;
50
- for (let i = 0; i < 8; i++) {
51
- data[1 + i] = Number(v & 0xffn);
52
- v >>= 8n;
53
- }
54
- return {
55
- programAddress: COMPUTE_BUDGET_PROGRAM,
56
- accounts: [],
57
- data,
58
- };
59
- }
11
+ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
12
+ import { appendTransactionMessageInstructions, compileTransaction, createTransactionMessage, getBase64EncodedWireTransaction, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
60
13
  /**
61
14
  * Build, sign, send, and confirm a transaction in one call.
62
15
  *
@@ -66,12 +19,17 @@ export function setComputeUnitPriceIx(microLamports) {
66
19
  export async function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment = 'confirmed', computeUnitLimit = 400_000, }) {
67
20
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
68
21
  const message = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([
69
- setComputeUnitLimitIx(computeUnitLimit),
22
+ getSetComputeUnitLimitInstruction({ units: computeUnitLimit }),
70
23
  // Always pin the priority fee (even at 0) so wallets like Phantom
71
- // don't silently append their own compute-budget instructions and
72
- // invalidate paired keypair-signer signatures. See
73
- // `setComputeUnitPriceIx` doc comment for the full story.
74
- setComputeUnitPriceIx(0n),
24
+ // don't silently *append* their own compute-budget instructions
25
+ // when the transaction is missing either limit or price. That
26
+ // mutation invalidates signatures already attached by paired
27
+ // keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
28
+ // producing `Transaction did not pass signature verification` on
29
+ // the validator. Pre-supplying both keeps the wallet from
30
+ // rewriting the message, so signatures over the original bytes
31
+ // still verify.
32
+ getSetComputeUnitPriceInstruction({ microLamports: 0n }),
75
33
  ...instructions,
76
34
  ], tx));
77
35
  const signedTx = await signTransactionMessageWithSigners(message);
@@ -16,13 +16,13 @@
16
16
  * helper is for.
17
17
  */
18
18
  import { addSignersToTransactionMessage, appendTransactionMessageInstructions, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
19
+ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
19
20
  import { SolanaANTRegistryWriteable } from './ant-registry-writeable.js';
20
21
  import { ARIO_ANT_PROGRAM_ID } from './constants.js';
21
22
  import { getInitializeInstructionAsync } from './generated/ant/instructions/index.js';
22
23
  import { getCreateV1Instruction } from './generated/mpl-core/instructions/index.js';
23
24
  import { DataState } from './generated/mpl-core/types/index.js';
24
25
  import { getAntRecordPDA } from './pda.js';
25
- import { setComputeUnitLimitIx, setComputeUnitPriceIx } from './send.js';
26
26
  /** AR.IO logo Arweave TX — matches the Rust default in `ario_ant::initialize`. */
27
27
  export const ARIO_LOGO_TX_ID = 'AnYvLJTWcG9lr2Ll5MwYWZR2o5uTE39WbpYB0zCxwKM';
28
28
  /**
@@ -181,12 +181,12 @@ export async function spawnSolanaANT(params) {
181
181
  // the mint signer on the message alongside the fee payer signer.
182
182
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
183
183
  const message = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([
184
- setComputeUnitLimitIx(computeUnitLimit),
184
+ getSetComputeUnitLimitInstruction({ units: computeUnitLimit }),
185
185
  // Pin the priority fee (at 0) so wallets like Phantom don't
186
186
  // silently append their own compute-budget instructions and
187
187
  // invalidate the paired mint keypair signer's signature. See
188
- // `setComputeUnitPriceIx` for the full story.
189
- setComputeUnitPriceIx(0n),
188
+ // `sendAndConfirm` in `./send.js` for the full rationale.
189
+ getSetComputeUnitPriceInstruction({ microLamports: 0n }),
190
190
  createIx,
191
191
  initIx,
192
192
  ...aclIxs,
@@ -14,4 +14,4 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
17
- export const version = '4.0.0-solana.4';
17
+ export const version = '4.0.0-solana.6';
@@ -316,4 +316,34 @@ export declare class SolanaARIOReadable {
316
316
  ttlSeconds: number;
317
317
  undernameLimit: number;
318
318
  }>;
319
+ /**
320
+ * Resolve the gateway operator pubkey backing a given observer pubkey.
321
+ * The `ObserverLookup` PDA is written at `join_network` (and rotated by
322
+ * `update_observer_address`); when present its `gateway` field is the
323
+ * operator pubkey. Returns `undefined` when the observer isn't
324
+ * registered on any gateway.
325
+ */
326
+ getObserverLookup(observer: Address): Promise<{
327
+ gateway: Address;
328
+ bump: number;
329
+ } | undefined>;
330
+ /**
331
+ * Pre-flight gate for `save_observations` submission. Reads the Epoch
332
+ * account once and reports whether the given observer pubkey is:
333
+ * - `prescribed`: in `epoch.prescribed_observers[..observer_count]`
334
+ * - `observerIdx`: position in the array (matches the `has_observed`
335
+ * bit index when prescribed)
336
+ * - `alreadyObserved`: whether the bit at `observerIdx` is set
337
+ * - `windowOpen`: whether `now < epoch.end_timestamp`
338
+ *
339
+ * Use this from a sink/wrapper to skip cheap-to-skip cases before
340
+ * paying for a transaction simulation that would just bounce.
341
+ */
342
+ getEpochObservationStatus(epochIndex: number, observer: Address): Promise<{
343
+ prescribed: boolean;
344
+ observerIdx: number;
345
+ alreadyObserved: boolean;
346
+ windowOpen: boolean;
347
+ endTimestampSec: number;
348
+ }>;
319
349
  }
@@ -58,6 +58,30 @@ export declare function splitPrimaryName(name: string): {
58
58
  * await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
59
59
  * ```
60
60
  */
61
+ /** Build the gateway_results bitmap for save_observations.
62
+ * All bits start as 1 (pass) for the first `registryAddresses.length`
63
+ * positions; positions named in `failedGateways` get cleared to 0; all
64
+ * positions beyond `registryAddresses.length` are 0. */
65
+ export declare function buildObservationBitmap(registryAddresses: string[], failedGateways: string[]): Buffer;
66
+ /** Encode an Arweave TX ID into the on-chain `[u8; 32]` slot.
67
+ *
68
+ * An Arweave TX ID **is** a 32-byte SHA-256 hash; the 43-char base64url
69
+ * string is just its presentation encoding. We decode here so the
70
+ * on-chain bytes are the raw hash — lossless and trivially reversible
71
+ * via base64url-encode on the consumer side. Without this, on-chain
72
+ * bytes alone couldn't be used to look up the original report bundle
73
+ * on permaweb (the whole point of recording the txid for auditability).
74
+ *
75
+ * Empty / undefined input → 32 zero bytes ("no permaweb archive
76
+ * configured for this submission" — the report still lives off-chain
77
+ * in the observer's local sinks but isn't anchored on Arweave).
78
+ *
79
+ * Throws on malformed input: the base64url string must be exactly 43
80
+ * chars and decode to 32 bytes. Strict validation here is desirable —
81
+ * silently truncating or accepting bad input would erode the
82
+ * auditability that the field exists for.
83
+ */
84
+ export declare function encodeReportTxId(reportTxId: string | undefined): Buffer;
61
85
  export declare class SolanaARIOWriteable extends SolanaARIOReadable {
62
86
  protected readonly signer: SolanaSigner;
63
87
  protected readonly rpcSubscriptions: SolanaRpcSubscriptions;
@@ -1,34 +1,5 @@
1
- /**
2
- * Shared helpers for building, signing, and sending Solana transactions
3
- * with @solana/kit. Used by SolanaARIOWriteable and SolanaANTWriteable.
4
- */
5
1
  import { type Commitment, type Instruction, type TransactionSigner } from '@solana/kit';
6
2
  import type { SolanaRpc, SolanaRpcSubscriptions } from './types.js';
7
- /**
8
- * Build a `SetComputeUnitLimit` instruction.
9
- *
10
- * Layout (per solana-program/compute-budget):
11
- * [0] u8 = 2 (discriminator for SetComputeUnitLimit)
12
- * [1..5] u32 LE = units
13
- */
14
- export declare function setComputeUnitLimitIx(units: number): Instruction;
15
- /**
16
- * Build a `SetComputeUnitPrice` instruction.
17
- *
18
- * Layout (per solana-program/compute-budget):
19
- * [0] u8 = 3 (discriminator for SetComputeUnitPrice)
20
- * [1..9] u64 LE = micro-lamports per compute unit
21
- *
22
- * We always prepend this (alongside `SetComputeUnitLimit`) before sending,
23
- * even with a 0 priority fee. Wallets like Phantom will silently *append*
24
- * their own compute-budget instructions when the transaction is missing
25
- * either, and that mutation invalidates any signatures already attached by
26
- * paired keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
27
- * producing `Transaction did not pass signature verification` on the
28
- * validator. Pre-supplying both keeps the wallet from rewriting the
29
- * message, so signatures over the original bytes still verify.
30
- */
31
- export declare function setComputeUnitPriceIx(microLamports: bigint | number): Instruction;
32
3
  /**
33
4
  * Build, sign, send, and confirm a transaction in one call.
34
5
  *
@@ -13,4 +13,4 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export declare const version = "4.0.0-solana.3";
16
+ export declare const version = "4.0.0-solana.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.0-solana.4",
3
+ "version": "4.0.0-solana.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"
@@ -9,6 +9,7 @@
9
9
  "module": "./lib/esm/node/index.js",
10
10
  "types": "./lib/types/node/index.d.ts",
11
11
  "type": "module",
12
+ "packageManager": "yarn@1.22.22",
12
13
  "engines": {
13
14
  "node": ">=18"
14
15
  },
@@ -133,6 +134,7 @@
133
134
  "@dha-team/arbundles": "^1.0.1",
134
135
  "@permaweb/aoconnect": "0.0.68",
135
136
  "@solana-program/compute-budget": "^0.15.0",
137
+ "@solana-program/token": "^0.13.0",
136
138
  "@solana/kit": "^6.8.0",
137
139
  "arweave": "1.15.5",
138
140
  "axios": "^1.13.2",