@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();
|
package/lib/esm/solana/send.js
CHANGED
|
@@ -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
|
-
//
|
|
40
|
-
// don't
|
|
41
|
-
//
|
|
42
|
-
// mutation invalidates
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
|
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
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
getSetComputeUnitPriceInstruction({ microLamports
|
|
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
|
-
|
|
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,
|
package/lib/esm/version.js
CHANGED
|
@@ -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
|
package/lib/types/version.d.ts
CHANGED