@ar.io/sdk 4.0.1 → 4.0.2-alpha.1

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.
@@ -1479,11 +1479,27 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
1479
1479
  extend: CostIntent.ExtendLease,
1480
1480
  increaseUndername: CostIntent.IncreaseUndernameLimit,
1481
1481
  };
1482
+ // IncreaseUndernameLimit pricing requires the record's purchase type
1483
+ // (lease vs permabuy) — undername cost differs by type, and the on-chain
1484
+ // instruction reads it from the record. `get_token_cost` can't read the
1485
+ // record (it only gets demandFactor + payer), so it prices from params and
1486
+ // REQUIRES `purchase_type` for this intent (else InvalidParameter #6039).
1487
+ // Pass the record's actual type so the estimate matches what the
1488
+ // instruction will charge.
1489
+ let purchaseType;
1490
+ if (args.operation === 'increaseUndername') {
1491
+ const record = await this.getArNSRecord({ name: args.params.name });
1492
+ purchaseType =
1493
+ record.type === 'permabuy'
1494
+ ? PurchaseType.Permabuy
1495
+ : PurchaseType.Lease;
1496
+ }
1482
1497
  const cost = await this._simulateTokenCost({
1483
1498
  intent: intentMap[args.operation],
1484
1499
  name: args.params.name,
1485
1500
  years: args.years,
1486
1501
  quantity: args.quantity,
1502
+ purchaseType,
1487
1503
  });
1488
1504
  const plan = await this._resolveFundingPlan(args.params, cost);
1489
1505
  const buyerATA = await getAssociatedTokenAddressKit(arnsConfig.mint, this.signer.address);
@@ -1757,9 +1773,21 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
1757
1773
  * funding-source slice to ario-gar's pay_from_funding_plan via CPI.
1758
1774
  */
1759
1775
  async _buildPrimaryNameFromFundingPlanIx(args) {
1776
+ // The primary-name fee is purchase-type-aware: the on-chain handler
1777
+ // (ario-core `primary_name_base_fee`) reads the base record's purchase_type
1778
+ // and charges 5x for permabuy vs lease. `get_token_cost` defaults a missing
1779
+ // purchase_type to Lease, so for a permabuy base name the estimate would be
1780
+ // 5x too low → FundingPlanAmountMismatch (#6066). Pass the base record's
1781
+ // actual type so the plan total matches what the program charges.
1782
+ const baseRecord = await this.getArNSRecord({
1783
+ name: splitPrimaryName(args.params.name).baseName,
1784
+ });
1760
1785
  const fee = await this._simulateTokenCost({
1761
1786
  intent: CostIntent.PrimaryNameRequest,
1762
1787
  name: args.params.name,
1788
+ purchaseType: baseRecord.type === 'permabuy'
1789
+ ? PurchaseType.Permabuy
1790
+ : PurchaseType.Lease,
1763
1791
  });
1764
1792
  const plan = await this._resolveFundingPlan(args.params, fee);
1765
1793
  const garConfig = await this.getGarConfig();
@@ -26,26 +26,67 @@
26
26
  import { ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS, getCloseLookupTableInstruction, getCreateLookupTableInstructionAsync, getDeactivateLookupTableInstruction, getExtendLookupTableInstruction, } from '@solana-program/address-lookup-table';
27
27
  import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
28
28
  import { appendTransactionMessageInstructions, compileTransaction, compressTransactionMessageUsingAddressLookupTables, createTransactionMessage, getAddressDecoder, getBase64EncodedWireTransaction, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
29
+ /**
30
+ * Floor for the auto-estimated priority fee (micro-lamports per CU). Ensures a
31
+ * non-zero fee so message-modifying wallets (Phantom) leave the tx alone. The
32
+ * absolute cost stays tiny: `fee ≈ price * CU_limit / 1e6` lamports
33
+ * (e.g. 10_000 µ£/CU × 400k CU ≈ 4_000 lamports ≈ 0.000004 SOL).
34
+ */
35
+ const MIN_PRIORITY_FEE_MICRO_LAMPORTS = 10000n;
36
+ /** Cap so a spiky fee market can't blow up the fee unexpectedly. */
37
+ const MAX_PRIORITY_FEE_MICRO_LAMPORTS = 2000000n;
38
+ /**
39
+ * Estimate a compute-unit price from recent on-chain prioritization fees: the
40
+ * 75th percentile of recent non-zero per-slot fees, clamped to
41
+ * [{@link MIN_PRIORITY_FEE_MICRO_LAMPORTS}, {@link MAX_PRIORITY_FEE_MICRO_LAMPORTS}].
42
+ * Falls back to the floor when there's no data or the query fails. Matching the
43
+ * going rate both lands the tx and keeps Phantom from bumping (and thus
44
+ * rewriting) the fee.
45
+ */
46
+ export async function estimatePriorityFeeMicroLamports(rpc) {
47
+ try {
48
+ const recent = await rpc.getRecentPrioritizationFees().send();
49
+ const fees = recent
50
+ .map((r) => BigInt(r.prioritizationFee))
51
+ .filter((f) => f > 0n)
52
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
53
+ if (fees.length === 0)
54
+ return MIN_PRIORITY_FEE_MICRO_LAMPORTS;
55
+ const p75 = fees[Math.min(fees.length - 1, Math.floor(fees.length * 0.75))];
56
+ if (p75 < MIN_PRIORITY_FEE_MICRO_LAMPORTS)
57
+ return MIN_PRIORITY_FEE_MICRO_LAMPORTS;
58
+ if (p75 > MAX_PRIORITY_FEE_MICRO_LAMPORTS)
59
+ return MAX_PRIORITY_FEE_MICRO_LAMPORTS;
60
+ return p75;
61
+ }
62
+ catch {
63
+ return MIN_PRIORITY_FEE_MICRO_LAMPORTS;
64
+ }
65
+ }
29
66
  /**
30
67
  * Build, sign, send, and confirm a transaction in one call.
31
68
  *
32
69
  * The caller supplies the core instructions; a compute-unit-limit instruction
33
70
  * is prepended automatically.
34
71
  */
35
- export async function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment = 'confirmed', computeUnitLimit = 400_000, addressLookupTables, }) {
72
+ export async function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment = 'confirmed', computeUnitLimit = 400_000, priorityFeeMicroLamports = 'auto', addressLookupTables, }) {
73
+ const microLamports = priorityFeeMicroLamports === 'auto'
74
+ ? await estimatePriorityFeeMicroLamports(rpc)
75
+ : priorityFeeMicroLamports === false
76
+ ? 0n
77
+ : BigInt(priorityFeeMicroLamports);
36
78
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
37
79
  const baseMessage = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([
38
80
  getSetComputeUnitLimitInstruction({ units: computeUnitLimit }),
39
- // Always pin the priority fee (even at 0) so wallets like Phantom
40
- // don't silently *append* their own compute-budget instructions
41
- // when the transaction is missing either limit or price. That
42
- // mutation invalidates signatures already attached by paired
43
- // keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
44
- // producing `Transaction did not pass signature verification` on
45
- // the validator. Pre-supplying both keeps the wallet from
46
- // rewriting the message, so signatures over the original bytes
47
- // still verify.
48
- getSetComputeUnitPriceInstruction({ microLamports: 0n }),
81
+ // Pin an explicit, NON-ZERO priority fee so wallets like Phantom
82
+ // don't rewrite the message to inject their own compute-budget
83
+ // instructions. Phantom treats a missing/zero fee as "unset" and
84
+ // overrides it on mainnet — that mutation invalidates the already-
85
+ // attached signatures ( "Transaction did not pass signature
86
+ // verification" / preflight #-32002). A real, network-rate fee
87
+ // (see `microLamports` above) makes the wallet leave the message
88
+ // alone, so signatures over the original bytes still verify.
89
+ getSetComputeUnitPriceInstruction({ microLamports }),
49
90
  ...instructions,
50
91
  ], tx));
51
92
  // Compress against any supplied lookup tables (v0). No-op when none given.
@@ -30,13 +30,14 @@
30
30
  * `import_account` instruction — that's intentional and not what this SDK
31
31
  * helper is for.
32
32
  */
33
- import { addSignersToTransactionMessage, appendTransactionMessageInstructions, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
33
+ import { addSignersToTransactionMessage, appendTransactionMessageInstructions, compileTransaction, createTransactionMessage, generateKeyPairSigner, getSignatureFromTransaction, isTransactionModifyingSigner, partiallySignTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
34
34
  import { getInitializeInstructionAsync } from '@ar.io/solana-contracts/ant';
35
35
  import { DataState, getCreateV1Instruction, } from '@ar.io/solana-contracts/mpl-core';
36
36
  import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
37
37
  import { SolanaANTRegistryWriteable } from './ant-registry-writeable.js';
38
38
  import { ARIO_ANT_PROGRAM_ID } from './constants.js';
39
39
  import { getAntRecordPDA } from './pda.js';
40
+ import { estimatePriorityFeeMicroLamports } from './send.js';
40
41
  /** AR.IO logo Arweave TX — matches the Rust default in `ario_ant::initialize`. */
41
42
  export const ARIO_LOGO_TX_ID = 'AnYvLJTWcG9lr2Ll5MwYWZR2o5uTE39WbpYB0zCxwKM';
42
43
  /**
@@ -193,14 +194,17 @@ export async function spawnSolanaANT(params) {
193
194
  // them up from the account metadata roles: accounts marked as SIGNER roles
194
195
  // must have a matching `TransactionSigner` attached. We do that by placing
195
196
  // the mint signer on the message alongside the fee payer signer.
196
- const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
197
+ const [{ value: latestBlockhash }, microLamports] = await Promise.all([
198
+ rpc.getLatestBlockhash().send(),
199
+ estimatePriorityFeeMicroLamports(rpc),
200
+ ]);
197
201
  const message = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([
198
202
  getSetComputeUnitLimitInstruction({ units: computeUnitLimit }),
199
- // Pin the priority fee (at 0) so wallets like Phantom don't
200
- // silently append their own compute-budget instructions and
201
- // invalidate the paired mint keypair signer's signature. See
202
- // `sendAndConfirm` in `./send.js` for the full rationale.
203
- getSetComputeUnitPriceInstruction({ microLamports: 0n }),
203
+ // Pin a non-zero priority fee (see `estimatePriorityFeeMicroLamports`).
204
+ // A real fee both lands the tx and, per Phantom's docs, stops the
205
+ // wallet from injecting its own fee. The paired mint-keypair signing
206
+ // is handled below.
207
+ getSetComputeUnitPriceInstruction({ microLamports }),
204
208
  createIx,
205
209
  initIx,
206
210
  ...aclIxs,
@@ -210,7 +214,26 @@ export async function spawnSolanaANT(params) {
210
214
  // metas and registers each matching signer by address, which is what
211
215
  // `signTransactionMessageWithSigners` then looks up to produce signatures.
212
216
  const withMintSigner = addSignersToTransactionMessage([mintSigner], message);
213
- const signedTx = await signTransactionMessageWithSigners(withMintSigner);
217
+ // Multi-signer spawn (fee-payer wallet + fresh mint keypair). Browser wallets
218
+ // like Phantom REWRITE transactions that carry no signature yet (injecting
219
+ // priority-fee / Lighthouse-guard instructions) — which invalidates the mint
220
+ // keypair's signature → "address is not a signer" (#5663015). Per Phantom's
221
+ // docs it leaves a transaction alone once it already has a signature. So when
222
+ // the wallet is a modifying signer, sign with the mint keypair FIRST, then let
223
+ // the wallet sign: it sees the existing mint signature and won't rewrite,
224
+ // keeping both signatures valid. kit's own pipeline can't express this order
225
+ // (it always runs modifying signers before partial ones), so we orchestrate
226
+ // it manually here. Non-modifying signers (keypairs in node/tests) carry no
227
+ // rewrite risk and use kit's normal pipeline.
228
+ let signedTx;
229
+ if (isTransactionModifyingSigner(signer)) {
230
+ const compiled = compileTransaction(withMintSigner);
231
+ const mintPreSigned = await partiallySignTransaction([mintSigner.keyPair], compiled);
232
+ [signedTx] = await signer.modifyAndSignTransactions([mintPreSigned]);
233
+ }
234
+ else {
235
+ signedTx = await signTransactionMessageWithSigners(withMintSigner);
236
+ }
214
237
  const sendAndConfirmFactory = sendAndConfirmTransactionFactory({
215
238
  rpc,
216
239
  rpcSubscriptions,
@@ -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.1';
17
+ export const version = '4.0.2-alpha.1';
@@ -1,18 +1,40 @@
1
1
  import { type Address, type Commitment, type Instruction, type TransactionSigner } from '@solana/kit';
2
2
  import type { SolanaRpc, SolanaRpcSubscriptions } from './types.js';
3
+ /**
4
+ * Estimate a compute-unit price from recent on-chain prioritization fees: the
5
+ * 75th percentile of recent non-zero per-slot fees, clamped to
6
+ * [{@link MIN_PRIORITY_FEE_MICRO_LAMPORTS}, {@link MAX_PRIORITY_FEE_MICRO_LAMPORTS}].
7
+ * Falls back to the floor when there's no data or the query fails. Matching the
8
+ * going rate both lands the tx and keeps Phantom from bumping (and thus
9
+ * rewriting) the fee.
10
+ */
11
+ export declare function estimatePriorityFeeMicroLamports(rpc: SolanaRpc): Promise<bigint>;
3
12
  /**
4
13
  * Build, sign, send, and confirm a transaction in one call.
5
14
  *
6
15
  * The caller supplies the core instructions; a compute-unit-limit instruction
7
16
  * is prepended automatically.
8
17
  */
9
- export declare function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment, computeUnitLimit, addressLookupTables, }: {
18
+ export declare function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment, computeUnitLimit, priorityFeeMicroLamports, addressLookupTables, }: {
10
19
  rpc: SolanaRpc;
11
20
  rpcSubscriptions: SolanaRpcSubscriptions;
12
21
  signer: TransactionSigner;
13
22
  instructions: Instruction[];
14
23
  commitment?: Commitment;
15
24
  computeUnitLimit?: number;
25
+ /**
26
+ * Compute-unit price (priority fee), in micro-lamports per CU.
27
+ * - `'auto'` (default): estimate from recent on-chain fees (see
28
+ * {@link estimatePriorityFeeMicroLamports}). A NON-ZERO fee is essential:
29
+ * wallets like Phantom treat a missing/zero fee as "unset" and rewrite the
30
+ * transaction to inject their own, which invalidates already-attached
31
+ * signatures (→ "Transaction did not pass signature verification"). A real,
32
+ * network-rate fee makes the wallet leave the message untouched.
33
+ * - a `number`/`bigint`: pin exactly this price.
34
+ * - `false`: no priority fee (price 0) — only for environments with no fee
35
+ * market (localnet) where wallet rewriting isn't a concern.
36
+ */
37
+ priorityFeeMicroLamports?: bigint | number | 'auto' | false;
16
38
  /**
17
39
  * Address Lookup Tables to compress the (v0) message against, as
18
40
  * `{ [tableAddress]: addresses }`. Accounts present in a table are referenced
@@ -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";
16
+ export declare const version = "4.0.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.1",
3
+ "version": "4.0.2-alpha.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"