@ar.io/sdk 4.0.0-alpha.1 → 4.0.0-alpha.3

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/README.md CHANGED
@@ -18,6 +18,7 @@ supplied by [`@ar.io/solana-contracts`](https://www.npmjs.com/package/@ar.io/sol
18
18
  - [Usage](#usage)
19
19
  - [ARIO Contract](#ario-contract)
20
20
  - [ANT Contracts](#ant-contracts)
21
+ - [Escrow](#escrow)
21
22
  - [Token Conversion](#token-conversion)
22
23
  - [Logging](#logging)
23
24
  - [Pagination](#pagination)
@@ -402,7 +403,7 @@ const ario = ARIO.init({
402
403
  On localnet (Surfpool) source program IDs from
403
404
  `migration/localnet/out/localnet.env` in the `solana-ar-io` monorepo.
404
405
 
405
- ##### Faucet
406
+ <!-- ##### Faucet
406
407
 
407
408
  The SDK exposes a `createFaucet` HTTP wrapper around the ar.io faucet
408
409
  service ([faucet.ar.io](https://faucet.ar.io)). The faucet backend has
@@ -475,7 +476,7 @@ if (
475
476
  }
476
477
  ```
477
478
 
478
- </details>
479
+ </details> -->
479
480
 
480
481
  ### Vaults
481
482
 
@@ -1996,6 +1997,40 @@ const observers = await ario.getPrescribedObservers({ epochIndex: 0 });
1996
1997
 
1997
1998
  </details>
1998
1999
 
2000
+ #### `crankEpochStep(options?)`
2001
+
2002
+ High-level, permissionless epoch crank. Advances the epoch lifecycle by **one
2003
+ step per call** and returns the action it took — run it on a loop (this is what
2004
+ the standalone cranker and the observer-embedded cranker do). It owns the whole
2005
+ sequence so you don't orchestrate the individual instructions yourself:
2006
+
2007
+ `create` → `tally` → `prescribe` → `distribute` → `close` — closing an epoch's
2008
+ observation PDAs first (`close_observation`) so `close_epoch` doesn't revert —
2009
+ plus an idle-tail of permissionless maintenance: `compound` delegate rewards,
2010
+ `update_demand_factor`, and `prune_returned_names`. The close path is
2011
+ non-wedging: a cleanup failure never blocks creation of the next epoch.
2012
+
2013
+ ```typescript
2014
+ const ario = ARIO.init({ rpc, rpcSubscriptions, signer });
2015
+
2016
+ // one step
2017
+ const result = await ario.crankEpochStep();
2018
+ // → { action, epochIndex?, txId?, progress? }
2019
+ // action ∈ create | tally | prescribe | distribute | close
2020
+ // | close_observation | compound | update_demand_factor
2021
+ // | prune_returned_names | idle
2022
+
2023
+ // or drive it on an interval
2024
+ setInterval(async () => {
2025
+ const r = await ario.crankEpochStep();
2026
+ if (r.action !== 'idle') console.log(r.action, r.epochIndex, r.txId);
2027
+ }, 60_000);
2028
+ ```
2029
+
2030
+ All options are optional: `batchSize`, `enableClose`, `epochRetention`,
2031
+ `enableCompound`, `compoundMinPendingRewards`, `enableDemandFactorRoll`,
2032
+ `enablePrune`, `pruneBatchSize`, `nameRegistryAccount`.
2033
+
1999
2034
  ### Primary Names
2000
2035
 
2001
2036
  #### `getPrimaryNames({ cursor, limit, sortBy, sortOrder })`
@@ -2229,7 +2264,7 @@ console.log(`ANT was spawned with module: ${moduleId}`);
2229
2264
 
2230
2265
  // With custom GraphQL URL and retries
2231
2266
  const moduleId = await ant.getModuleId({
2232
- graphqlUrl: "https://arweave.net/graphql",
2267
+ graphqlUrl: "https://turbo-gateway.com/graphql",
2233
2268
  retries: 5,
2234
2269
  });
2235
2270
  ```
@@ -2630,7 +2665,7 @@ const balance = await ant.getBalance({
2630
2665
 
2631
2666
  #### `transfer({ target })`
2632
2667
 
2633
- Transfers ownership of the ANT to a new target address. Target MUST be an Arweave address.
2668
+ Transfers ownership of the ANT to a new target address. Target must be a Solana address. ANT transfers are standard Metaplex Core NFT transfers.
2634
2669
 
2635
2670
  _Note: Requires `signer` to be provided on `ANT.init` to sign the transaction._
2636
2671
 
@@ -3026,6 +3061,57 @@ forking + name reassignment). On Solana, schema migration is a
3026
3061
  per-asset CPI exposed as the instance method `ant.upgrade()` documented
3027
3062
  above; new ANTs are created with `ANT.spawn()`.
3028
3063
 
3064
+ ## Escrow
3065
+
3066
+ Trustless, multi-protocol escrow for handing an asset to a recipient identified by
3067
+ an **Arweave** or **Ethereum** address, claimable once they hold a Solana wallet.
3068
+ Backed by the `ario-ant-escrow` program. Two clients:
3069
+
3070
+ - `TokenEscrow` — escrow liquid **ARIO** (SPL) or a **time-locked vault**.
3071
+ - `ANTEscrow` — escrow an **ANT** (Metaplex Core NFT).
3072
+
3073
+ Each supports **deposit → claim → cancel/refund → update-recipient**. Claims work
3074
+ three ways: **Arweave-attested** (an off-chain attestor re-signs the canonical
3075
+ claim with Ed25519, verified on-chain), **Ethereum** (on-chain `secp256k1_recover`
3076
+ + EIP-191), and **vault** (instruction introspection that preserves the remaining
3077
+ lock).
3078
+
3079
+ ```typescript
3080
+ import { TokenEscrow, canonicalMessageV2 } from '@ar.io/sdk';
3081
+
3082
+ const escrow = new TokenEscrow({
3083
+ rpc,
3084
+ rpcSubscriptions,
3085
+ signer,
3086
+ programId,
3087
+ coreProgram,
3088
+ });
3089
+
3090
+ // deposit 50 ARIO to an Ethereum recipient
3091
+ await escrow.depositTokens({
3092
+ assetId, // 32-byte client-supplied id
3093
+ amount: 50_000_000n,
3094
+ arioMint,
3095
+ depositorTokenAccount,
3096
+ recipient: { protocol: 'ethereum', publicKey: ethAddress20 },
3097
+ });
3098
+
3099
+ // the recipient claims (Ethereum path) once they have a Solana wallet
3100
+ await escrow.claimTokensEthereum({
3101
+ depositor,
3102
+ assetId,
3103
+ claimant,
3104
+ claimantTokenAccount,
3105
+ escrowTokenAccount,
3106
+ signature, // recipient's EIP-191 signature over canonicalMessageV2(...)
3107
+ });
3108
+ ```
3109
+
3110
+ Build the exact bytes a recipient signs with `canonicalMessage` /
3111
+ `canonicalMessageV2` — byte-identical to the on-chain program (and the off-chain
3112
+ attestor). See the contracts repo's escrow design + protocol spec for the full
3113
+ flow and the cross-language canonical-message vectors.
3114
+
3029
3115
  ## Token Conversion
3030
3116
 
3031
3117
  The ARIO process stores all values as mARIO (micro-ARIO) to avoid floating-point arithmetic issues. The SDK provides an `ARIOToken` and `mARIOToken` classes to handle the conversion between ARIO and mARIO, along with rounding logic for precision.
@@ -3258,7 +3344,7 @@ import {
3258
3344
 
3259
3345
  | Network | RPC | Programs |
3260
3346
  |---|---|---|
3261
- | Mainnet | not yet deployed | TBD |
3347
+ | Mainnet | `https://api.mainnet-beta.solana.com` (mainnet-beta, default) | Not yet deployed placeholder IDs in `src/solana/constants.ts` |
3262
3348
  | Devnet | `https://api.devnet.solana.com` | See `src/solana/constants.ts` for current devnet program IDs |
3263
3349
  | Localnet | Surfpool — `https://github.com/solana-foundation/surfpool` | Localnet harness in `solana-ar-io` monorepo |
3264
3350
 
@@ -3336,4 +3422,4 @@ For more information on how to contribute, please see [CONTRIBUTING.md].
3336
3422
 
3337
3423
  ```
3338
3424
 
3339
- ```
3425
+ ```
@@ -24,7 +24,7 @@ export async function setAntRecordCLICommand(o) {
24
24
  if (!o.skipConfirmation) {
25
25
  await assertConfirmationPrompt(`Are you sure you want to set this record on the ANT process ${writeAnt.processId}?\n${JSON.stringify(recordParams, null, 2)}`, o);
26
26
  }
27
- return (await writeANTFromOptions(o)).setRecord(recordParams, customTagsFromOptions(o));
27
+ return writeAnt.setRecord(recordParams, customTagsFromOptions(o));
28
28
  }
29
29
  export async function setAntBaseNameCLICommand(o) {
30
30
  const ttlSeconds = +(o.ttlSeconds ?? defaultTtlSecondsCLI);
@@ -40,7 +40,7 @@ export async function setAntBaseNameCLICommand(o) {
40
40
  if (!o.skipConfirmation) {
41
41
  await assertConfirmationPrompt(`Are you sure you want to set this base name on the ANT process ${writeAnt.processId}?\n${JSON.stringify(params, null, 2)}`, o);
42
42
  }
43
- return (await writeANTFromOptions(o)).setBaseNameRecord(params, customTagsFromOptions(o));
43
+ return writeAnt.setBaseNameRecord(params, customTagsFromOptions(o));
44
44
  }
45
45
  export async function setAntUndernameCLICommand(o) {
46
46
  const ttlSeconds = +(o.ttlSeconds ?? defaultTtlSecondsCLI);
@@ -58,7 +58,7 @@ export async function setAntUndernameCLICommand(o) {
58
58
  if (!o.skipConfirmation) {
59
59
  await assertConfirmationPrompt(`Are you sure you want to set this undername on the ANT process ${writeAnt.processId}?\n${JSON.stringify(params, null, 2)}`, o);
60
60
  }
61
- return (await writeANTFromOptions(o)).setUndernameRecord(params, customTagsFromOptions(o));
61
+ return writeAnt.setUndernameRecord(params, customTagsFromOptions(o));
62
62
  }
63
63
  export async function transferRecordOwnershipCLICommand(o) {
64
64
  const undername = requiredStringFromOptions(o, 'undername');
@@ -67,5 +67,5 @@ export async function transferRecordOwnershipCLICommand(o) {
67
67
  if (!o.skipConfirmation) {
68
68
  await assertConfirmationPrompt(`Are you sure you want to transfer ownership of "${undername}" to "${recipient}" on ANT process ${writeAnt.processId}?\n${JSON.stringify({ undername, recipient }, null, 2)}`, o);
69
69
  }
70
- return (await writeANTFromOptions(o)).transferRecord({ undername, recipient }, customTagsFromOptions(o));
70
+ return writeAnt.transferRecord({ undername, recipient }, customTagsFromOptions(o));
71
71
  }
@@ -192,12 +192,18 @@ export async function escrowClaimArweaveCLICommand(o) {
192
192
  if (sigBytes.length !== 512) {
193
193
  throw new Error(`signature file must be 512 bytes; got ${sigBytes.length}`);
194
194
  }
195
+ // RSA-PSS salt length: 0 is a valid value, so only reject NaN / negative /
196
+ // non-integer. An unset flag defaults to 32 (a full SHA-256 digest).
197
+ const saltLen = o.saltLen ? Number.parseInt(o.saltLen, 10) : 32;
198
+ if (!Number.isInteger(saltLen) || saltLen < 0) {
199
+ throw new Error(`--salt-len must be a non-negative integer (got '${o.saltLen}')`);
200
+ }
195
201
  const escrow = await writeEscrowFromOptions(o);
196
202
  const sig = await escrow.claimArweave({
197
203
  antMint: antFromOptions(o),
198
204
  claimant: address(o.claimant),
199
205
  signature: sigBytes,
200
- saltLen: o.saltLen ? Number(o.saltLen) : 32,
206
+ saltLen,
201
207
  });
202
208
  return { signature: sig };
203
209
  }
@@ -74,7 +74,7 @@ export async function leaveNetwork(options) {
74
74
  '\n\n' +
75
75
  'Are you sure you want to leave the AR.IO network?', options);
76
76
  }
77
- return (await writeARIOFromOptions(options)).ario.leaveNetwork(customTagsFromOptions(options));
77
+ return ario.leaveNetwork(customTagsFromOptions(options));
78
78
  }
79
79
  export async function saveObservations(o) {
80
80
  const failedGateways = requiredStringArrayFromOptions(o, 'failedGateways');
@@ -232,18 +232,17 @@ export async function delegateStake(options) {
232
232
  return output;
233
233
  }
234
234
  export async function decreaseDelegateStake(options) {
235
- const ario = await (await writeARIOFromOptions(options)).ario;
235
+ const { ario, signerAddress } = await writeARIOFromOptions(options);
236
236
  const { target, arioQuantity } = requiredTargetAndQuantityFromOptions(options);
237
237
  const instant = options.instant ?? false;
238
238
  if (!options.skipConfirmation) {
239
239
  // Verify the target gateway exists and look up delegation
240
- const signerAddr = (await writeARIOFromOptions(options)).signerAddress;
241
240
  const targetGateway = await ario.getGateway({ address: target });
242
241
  if (targetGateway === undefined) {
243
242
  throw new Error(`Gateway not found: ${target}`);
244
243
  }
245
244
  // Find delegation to this gateway
246
- const delegations = await ario.getDelegations({ address: signerAddr });
245
+ const delegations = await ario.getDelegations({ address: signerAddress });
247
246
  const delegation = delegations.items.find((d) => d.gatewayAddress === target && d.type === 'stake');
248
247
  if (!delegation) {
249
248
  throw new Error(`No active delegation found for you on gateway ${target}.`);
@@ -279,7 +278,7 @@ export async function decreaseDelegateStake(options) {
279
278
  return output;
280
279
  }
281
280
  export async function redelegateStake(options) {
282
- const ario = await (await writeARIOFromOptions(options)).ario;
281
+ const { ario, signerAddress } = await writeARIOFromOptions(options);
283
282
  const params = redelegateParamsFromOptions(options);
284
283
  if (!options.skipConfirmation) {
285
284
  const targetGateway = await ario.getGateway({ address: params.target });
@@ -293,8 +292,7 @@ export async function redelegateStake(options) {
293
292
  if (sourceGateway === undefined) {
294
293
  throw new Error(`Source gateway not found: ${params.source}`);
295
294
  }
296
- const signerAddr = (await writeARIOFromOptions(options)).signerAddress;
297
- const delegations = await ario.getDelegations({ address: signerAddr });
295
+ const delegations = await ario.getDelegations({ address: signerAddress });
298
296
  const delegation = delegations.items.find((d) => d.gatewayAddress === params.source && d.type === 'stake');
299
297
  if (!delegation || delegation.balance < params.stakeQty.valueOf()) {
300
298
  const available = delegation?.balance ?? 0;
@@ -3,9 +3,15 @@ async function getSolanaWriter(o) {
3
3
  const { ario } = await writeARIOFromOptions(o);
4
4
  return ario;
5
5
  }
6
- function parseMaxNames(o) {
6
+ function parseMaxNames(o, fallbackCount) {
7
7
  const raw = o.max;
8
8
  if (raw === undefined) {
9
+ // When an explicit name list is supplied, derive the u8 batch size from
10
+ // it (capped at the on-chain max of 255) so callers don't have to also
11
+ // pass --max. Discovery paths (no explicit list) still require --max.
12
+ if (fallbackCount !== undefined && fallbackCount > 0) {
13
+ return Math.min(fallbackCount, 255);
14
+ }
9
15
  throw new Error('--max <count> is required (u8 batch size, 1-255)');
10
16
  }
11
17
  const n = Number(raw);
@@ -18,7 +24,7 @@ function parseMaxNames(o) {
18
24
  // ArNS prune
19
25
  // =========================================
20
26
  export async function pruneExpiredNamesCLICommand(o) {
21
- const max = parseMaxNames(o);
27
+ const max = parseMaxNames(o, o.arnsRecords?.length);
22
28
  const ario = await getSolanaWriter(o);
23
29
  // If `--arns-records` wasn't provided, discover them via the readable's
24
30
  // helper. Cap at `max` so we never overshoot the ix's u8 batch parameter.
@@ -44,7 +50,7 @@ export async function pruneNameToReturnedCLICommand(o) {
44
50
  return ario.pruneNameToReturned({ name });
45
51
  }
46
52
  export async function pruneReturnedNamesCLICommand(o) {
47
- const max = parseMaxNames(o);
53
+ const max = parseMaxNames(o, o.returnedNames?.length);
48
54
  const ario = await getSolanaWriter(o);
49
55
  let returned = o.returnedNames ?? [];
50
56
  if (returned.length === 0) {
@@ -129,13 +135,11 @@ export async function releaseVaultCLICommand(o) {
129
135
  // configured signer as the owner. The `--owner` flag is accepted as
130
136
  // documentation but ignored unless it matches the signer; fail loud if
131
137
  // it doesn't, so users don't think they can release someone else's vault.
132
- const ario = await getSolanaWriter(o);
133
- if (o.owner) {
134
- const signerAddr = (await writeARIOFromOptions(o)).signerAddress;
135
- if (o.owner !== signerAddr) {
136
- throw new Error(`release-vault: --owner ${o.owner} does not match signer ${signerAddr}. ` +
137
- `release_vault is owner-signed; use the owner's wallet to call it.`);
138
- }
138
+ const { ario: arioWrite, signerAddress } = await writeARIOFromOptions(o);
139
+ const ario = arioWrite;
140
+ if (o.owner && o.owner !== signerAddress) {
141
+ throw new Error(`release-vault: --owner ${o.owner} does not match signer ${signerAddress}. ` +
142
+ `release_vault is owner-signed; use the owner's wallet to call it.`);
139
143
  }
140
144
  const vaultIdStr = requiredStringFromOptions(o, 'vaultId');
141
145
  const vaultId = vaultIdStr;
@@ -248,7 +248,7 @@ export function requiredAddressFromOptions(options) {
248
248
  if (address !== undefined) {
249
249
  return address;
250
250
  }
251
- throw new Error('No address provided. Use --address or --wallet-file');
251
+ throw new Error('No address provided. Use --address, --wallet-file, or --private-key');
252
252
  }
253
253
  const defaultCliPaginationLimit = 10; // more friendly UX than 100
254
254
  export function paginationParamsFromOptions(options) {
@@ -583,16 +583,25 @@ export function fundFromFromOptions(o) {
583
583
  export function referrerFromOptions(o) {
584
584
  return o.referrer;
585
585
  }
586
+ /** Largest value representable by an unsigned 64-bit integer (2^64 - 1). */
587
+ const U64_MAX = (1n << 64n) - 1n;
586
588
  /** Parse `--withdrawal-id` from CLI options into a bigint. */
587
589
  export function withdrawalIdFromOptions(o) {
588
590
  if (o.withdrawalId === undefined)
589
591
  return undefined;
592
+ let id;
590
593
  try {
591
- return BigInt(o.withdrawalId);
594
+ id = BigInt(o.withdrawalId);
592
595
  }
593
596
  catch {
594
597
  throw new Error(`Invalid --withdrawal-id: '${o.withdrawalId}' is not a valid u64 integer`);
595
598
  }
599
+ // The on-chain seed encoder treats the id as a u64; reject out-of-range
600
+ // values here with a clear message instead of a downstream encoder error.
601
+ if (id < 0n || id > U64_MAX) {
602
+ throw new Error(`Invalid --withdrawal-id: '${o.withdrawalId}' is outside u64 range (0..${U64_MAX})`);
603
+ }
604
+ return id;
596
605
  }
597
606
  /**
598
607
  * Parse `--funding-plan-json` into a `FundingSourceSpec[]`. Validates each
@@ -647,6 +656,9 @@ export function fundingPlanFromOptions(o) {
647
656
  if (amount <= 0n) {
648
657
  throw new Error(`--funding-plan-json[${idx}].amount must be > 0 (got ${e.amount})`);
649
658
  }
659
+ if (amount > U64_MAX) {
660
+ throw new Error(`--funding-plan-json[${idx}].amount '${e.amount}' exceeds u64 max (${U64_MAX})`);
661
+ }
650
662
  const out = {
651
663
  kind: e.kind,
652
664
  amount,
@@ -658,6 +670,16 @@ export function fundingPlanFromOptions(o) {
658
670
  if (e.kind !== 'delegation' && e.kind !== 'operatorStake') {
659
671
  throw new Error(`--funding-plan-json[${idx}].gateway is only valid for kind 'delegation' or 'operatorStake' (got '${e.kind}')`);
660
672
  }
673
+ // The gateway is going to be used: confirm it actually decodes as a
674
+ // base58 Solana address. `address()` throws on malformed input, so a
675
+ // bad string fails the CLI here rather than deep in instruction
676
+ // building.
677
+ try {
678
+ address(e.gateway);
679
+ }
680
+ catch {
681
+ throw new Error(`--funding-plan-json[${idx}].gateway '${e.gateway}' is not a valid base58 Solana address`);
682
+ }
661
683
  out.gateway = e.gateway;
662
684
  }
663
685
  return out;
@@ -11,6 +11,12 @@ const DEFAULT_FAUCET_API_URL = 'https://faucet.ario.permaweb.services';
11
11
  * id today; will likely become a Solana mint pubkey).
12
12
  */
13
13
  export function createFaucet({ arioInstance, faucetApiUrl = DEFAULT_FAUCET_API_URL, processId, }) {
14
+ // The backend has no implicit default for `processId`; fail at the API
15
+ // boundary so callers get immediate feedback instead of an opaque HTTP
16
+ // error on the first faucet call.
17
+ if (typeof processId !== 'string' || processId.trim() === '') {
18
+ throw new Error('createFaucet: `processId` is required and must be non-empty');
19
+ }
14
20
  const faucet = new ARIOTokenFaucet({
15
21
  faucetUrl: faucetApiUrl,
16
22
  processId,
@@ -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-alpha.1';
17
+ export const version = '4.0.0-alpha.3';
@@ -44,6 +44,26 @@ export type ARIOConfig = {
44
44
  };
45
45
  export declare const DEFAULT_SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com";
46
46
  export declare class ARIO {
47
+ /**
48
+ * Create an ARIO client bound to a Solana RPC transport.
49
+ *
50
+ * The return type is selected by the {@link ARIOConfig} you pass:
51
+ * - **Read-write** — when `signer` (a {@link SolanaSigner}) is provided. A
52
+ * {@link SolanaRpcSubscriptions} client is then also required so
53
+ * `@solana/kit`'s `sendAndConfirmTransaction` can await confirmations;
54
+ * omitting it throws. Returns {@link ARIOWrite}.
55
+ * - **Read-only** — when `signer` is omitted. Returns {@link ARIORead}.
56
+ *
57
+ * Program-id overrides (`coreProgramId` / `garProgramId` / `arnsProgramId` /
58
+ * `antProgramId`) are required on any non-mainnet cluster (devnet, localnet,
59
+ * Surfpool); see {@link ARIOConfig}.
60
+ *
61
+ * @param config - RPC transport, optional signer/subscriptions, and
62
+ * per-cluster program-id overrides.
63
+ * @returns {@link ARIOWrite} when a signer is supplied, otherwise
64
+ * {@link ARIORead}.
65
+ * @throws If a signer is supplied without `rpcSubscriptions`.
66
+ */
47
67
  static init(config: ARIOConfig & {
48
68
  signer: SolanaSigner;
49
69
  rpcSubscriptions: SolanaRpcSubscriptions;
@@ -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.40";
16
+ export declare const version = "4.0.0-alpha.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.0-alpha.1",
3
+ "version": "4.0.0-alpha.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"
@@ -114,7 +114,7 @@
114
114
  "typescript": "^5.1.6"
115
115
  },
116
116
  "dependencies": {
117
- "@ar.io/solana-contracts": "0.8.0-staging.18",
117
+ "@ar.io/solana-contracts": "1.0.0",
118
118
  "@noble/hashes": "^1.8.0",
119
119
  "@solana-program/address-lookup-table": "^0.11.0",
120
120
  "@solana-program/compute-budget": "^0.15.0",