@ar.io/sdk 4.0.0-solana.5 → 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.
- package/lib/cjs/solana/deserialize.js +44 -0
- package/lib/cjs/solana/io-readable.js +66 -1
- package/lib/cjs/solana/io-writeable.js +86 -15
- package/lib/cjs/solana/send.js +17 -61
- package/lib/cjs/solana/spawn-ant.js +4 -4
- package/lib/cjs/version.js +1 -1
- package/lib/esm/solana/deserialize.js +44 -0
- package/lib/esm/solana/io-readable.js +67 -2
- package/lib/esm/solana/io-writeable.js +84 -15
- package/lib/esm/solana/send.js +17 -59
- package/lib/esm/solana/spawn-ant.js +4 -4
- package/lib/esm/version.js +1 -1
- package/lib/types/solana/io-readable.d.ts +30 -0
- package/lib/types/solana/io-writeable.d.ts +24 -0
- package/lib/types/solana/send.d.ts +0 -29
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
*
|
|
@@ -148,6 +150,79 @@ function splitPrimaryName(name) {
|
|
|
148
150
|
* await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
|
|
149
151
|
* ```
|
|
150
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
|
+
}
|
|
151
226
|
class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
|
|
152
227
|
signer;
|
|
153
228
|
rpcSubscriptions;
|
|
@@ -603,22 +678,9 @@ class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
|
|
|
603
678
|
else {
|
|
604
679
|
const registryAddresses = await this.getRegistryGatewayAddresses();
|
|
605
680
|
gatewayCount = registryAddresses.length;
|
|
606
|
-
resultsBuf =
|
|
607
|
-
const failedSet = new Set(params.failedGateways);
|
|
608
|
-
for (let i = 0; i < gatewayCount; i++) {
|
|
609
|
-
if (failedSet.has(registryAddresses[i])) {
|
|
610
|
-
resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
// Clear bits beyond the active gateway count.
|
|
614
|
-
for (let i = gatewayCount; i < 3000; i++) {
|
|
615
|
-
resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
616
|
-
}
|
|
681
|
+
resultsBuf = buildObservationBitmap(registryAddresses, params.failedGateways);
|
|
617
682
|
}
|
|
618
|
-
|
|
619
|
-
// truncates the raw string bytes; mirror that for compatibility.
|
|
620
|
-
const reportTxId = Buffer.alloc(32);
|
|
621
|
-
Buffer.from(params.reportTxId).copy(reportTxId, 0, 0, Math.min(32, params.reportTxId.length));
|
|
683
|
+
const reportTxId = encodeReportTxId(params.reportTxId);
|
|
622
684
|
const ix = await (0, index_js_5.getSaveObservationsInstructionAsync)({
|
|
623
685
|
observer: this.signer,
|
|
624
686
|
epochIndex: BigInt(epochIndex),
|
|
@@ -1867,9 +1929,18 @@ class SolanaARIOWriteable extends io_readable_js_1.SolanaARIOReadable {
|
|
|
1867
1929
|
*/
|
|
1868
1930
|
async distributeEpoch(params, _options) {
|
|
1869
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);
|
|
1870
1939
|
const ix = await (0, index_js_5.getDistributeEpochInstructionAsync)(await this.withGarDefaults({
|
|
1871
1940
|
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
1872
1941
|
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1942
|
+
arioConfig,
|
|
1943
|
+
arioCoreProgram: this.coreProgram,
|
|
1873
1944
|
payer: this.signer,
|
|
1874
1945
|
epochIndex: BigInt(params.epochIndex),
|
|
1875
1946
|
}), { programAddress: this.garProgram });
|
package/lib/cjs/solana/send.js
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
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,
|
|
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
|
-
// `
|
|
194
|
-
(0,
|
|
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,
|
package/lib/cjs/version.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -111,6 +111,79 @@ export function splitPrimaryName(name) {
|
|
|
111
111
|
* await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
|
|
112
112
|
* ```
|
|
113
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
|
+
}
|
|
114
187
|
export class SolanaARIOWriteable extends SolanaARIOReadable {
|
|
115
188
|
signer;
|
|
116
189
|
rpcSubscriptions;
|
|
@@ -566,22 +639,9 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
|
|
|
566
639
|
else {
|
|
567
640
|
const registryAddresses = await this.getRegistryGatewayAddresses();
|
|
568
641
|
gatewayCount = registryAddresses.length;
|
|
569
|
-
resultsBuf =
|
|
570
|
-
const failedSet = new Set(params.failedGateways);
|
|
571
|
-
for (let i = 0; i < gatewayCount; i++) {
|
|
572
|
-
if (failedSet.has(registryAddresses[i])) {
|
|
573
|
-
resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
// Clear bits beyond the active gateway count.
|
|
577
|
-
for (let i = gatewayCount; i < 3000; i++) {
|
|
578
|
-
resultsBuf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
579
|
-
}
|
|
642
|
+
resultsBuf = buildObservationBitmap(registryAddresses, params.failedGateways);
|
|
580
643
|
}
|
|
581
|
-
|
|
582
|
-
// truncates the raw string bytes; mirror that for compatibility.
|
|
583
|
-
const reportTxId = Buffer.alloc(32);
|
|
584
|
-
Buffer.from(params.reportTxId).copy(reportTxId, 0, 0, Math.min(32, params.reportTxId.length));
|
|
644
|
+
const reportTxId = encodeReportTxId(params.reportTxId);
|
|
585
645
|
const ix = await getSaveObservationsInstructionAsync({
|
|
586
646
|
observer: this.signer,
|
|
587
647
|
epochIndex: BigInt(epochIndex),
|
|
@@ -1830,9 +1890,18 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
|
|
|
1830
1890
|
*/
|
|
1831
1891
|
async distributeEpoch(params, _options) {
|
|
1832
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);
|
|
1833
1900
|
const ix = await getDistributeEpochInstructionAsync(await this.withGarDefaults({
|
|
1834
1901
|
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
1835
1902
|
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1903
|
+
arioConfig,
|
|
1904
|
+
arioCoreProgram: this.coreProgram,
|
|
1836
1905
|
payer: this.signer,
|
|
1837
1906
|
epochIndex: BigInt(params.epochIndex),
|
|
1838
1907
|
}), { programAddress: this.garProgram });
|
package/lib/esm/solana/send.js
CHANGED
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
// `
|
|
189
|
-
|
|
188
|
+
// `sendAndConfirm` in `./send.js` for the full rationale.
|
|
189
|
+
getSetComputeUnitPriceInstruction({ microLamports: 0n }),
|
|
190
190
|
createIx,
|
|
191
191
|
initIx,
|
|
192
192
|
...aclIxs,
|
package/lib/esm/version.js
CHANGED
|
@@ -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
|
*
|
package/lib/types/version.d.ts
CHANGED