Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format
## Table of Contents

- [Unreleased](#unreleased)
- [1.7.8 - 2025-10-02](#178---2025-10-02)
- [1.7.6 - 2025-09-09](#176---2025-09-08)
- [1.7.5 - 2025-09-08](#175---2025-09-08)
- [1.7.4 - 2025-09-08](#174---2025-09-08)
Expand Down Expand Up @@ -157,6 +158,15 @@ All notable changes to this project will be documented in this file. The format

### Security

---

### [1.7.8] - 2025-10-02

### Added

- **LivePolicy fee model**: New dynamic fee model that fetches current rates from ARC GorillaPool API (`https://arc.gorillapool.io/v1/policy`) with intelligent caching and fallback mechanisms


---

### [1.7.7] - 2025-09-19
Expand Down
128 changes: 73 additions & 55 deletions docs/reference/transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,9 +456,9 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
| [BeefParty](#class-beefparty) |
| [BeefTx](#class-beeftx) |
| [FetchHttpClient](#class-fetchhttpclient) |
| [LivePolicy](#class-livepolicy) |
| [MerklePath](#class-merklepath) |
| [NodejsHttpClient](#class-nodejshttpclient) |
| [SatoshisPerKilobyte](#class-satoshisperkilobyte) |
| [Transaction](#class-transaction) |
| [WhatsOnChain](#class-whatsonchain) |

Expand Down Expand Up @@ -1198,6 +1198,72 @@ See also: [Fetch](./transaction.md#type-fetch), [HttpClient](./transaction.md#in

Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)

---
### Class: LivePolicy

Represents a live fee policy that fetches current rates from ARC GorillaPool.

```ts
export default class LivePolicy implements FeeModel {
constructor(cacheValidityMs: number = 5 * 60 * 1000)
static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
async computeFee(tx: Transaction): Promise<number>
}
```

See also: [FeeModel](./transaction.md#interface-feemodel), [Transaction](./transaction.md#class-transaction)

#### Constructor

Constructs an instance of the live policy fee model.

```ts
constructor(cacheValidityMs: number = 5 * 60 * 1000)
```

Argument Details

+ **cacheValidityMs**
+ How long to cache the fee rate in milliseconds (default: 5 minutes)

#### Method computeFee

Computes the fee for a given transaction using the current live rate.

```ts
async computeFee(tx: Transaction): Promise<number>
```
See also: [Transaction](./transaction.md#class-transaction)

Returns

The fee in satoshis for the transaction.

Argument Details

+ **tx**
+ The transaction for which a fee is to be computed.

#### Method getInstance

Gets the singleton instance of LivePolicy to ensure cache sharing across the application.

```ts
static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
```
See also: [LivePolicy](./transaction.md#class-livepolicy)

Returns

The singleton LivePolicy instance

Argument Details

+ **cacheValidityMs**
+ How long to cache the fee rate in milliseconds (default: 5 minutes)

Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)

---
### Class: MerklePath

Expand Down Expand Up @@ -1420,54 +1486,6 @@ See also: [HttpClient](./transaction.md#interface-httpclient), [HttpClientReques

Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)

---
### Class: SatoshisPerKilobyte

Represents the "satoshis per kilobyte" transaction fee model.

```ts
export default class SatoshisPerKilobyte implements FeeModel {
value: number;
constructor(value: number)
async computeFee(tx: Transaction): Promise<number>
}
```

See also: [FeeModel](./transaction.md#interface-feemodel), [Transaction](./transaction.md#class-transaction)

#### Constructor

Constructs an instance of the sat/kb fee model.

```ts
constructor(value: number)
```

Argument Details

+ **value**
+ The number of satoshis per kilobyte to charge as a fee.

#### Method computeFee

Computes the fee for a given transaction.

```ts
async computeFee(tx: Transaction): Promise<number>
```
See also: [Transaction](./transaction.md#class-transaction)

Returns

The fee in satoshis for the transaction, as a BigNumber.

Argument Details

+ **tx**
+ The transaction for which a fee is to be computed.

Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)

---
### Class: Transaction

Expand Down Expand Up @@ -1520,7 +1538,7 @@ export default class Transaction {
addOutput(output: TransactionOutput): void
addP2PKHOutput(address: number[] | string, satoshis?: number): void
updateMetadata(metadata: Record<string, any>): void
async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
getFee(): number
async sign(): Promise<void>
async broadcast(broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure>
Expand All @@ -1540,7 +1558,7 @@ export default class Transaction {
}
```

See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)
See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)

#### Method addInput

Expand Down Expand Up @@ -1610,13 +1628,13 @@ Argument Details
#### Method fee

Computes fees prior to signing.
If no fee model is provided, uses a SatoshisPerKilobyte fee model that pays 1 sat/kb.
If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
If fee is a number, the transaction uses that value as fee.

```ts
async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
```
See also: [FeeModel](./transaction.md#interface-feemodel), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte)
See also: [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy)

Argument Details

Expand Down Expand Up @@ -2037,7 +2055,7 @@ Argument Details
Example

```ts
tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
```

Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
Expand Down
8 changes: 4 additions & 4 deletions src/transaction/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import LockingScript from '../script/LockingScript.js'
import { Reader, Writer, toHex, toArray } from '../primitives/utils.js'
import { hash256 } from '../primitives/Hash.js'
import FeeModel from './FeeModel.js'
import SatoshisPerKilobyte from './fee-models/SatoshisPerKilobyte.js'
import LivePolicy from './fee-models/LivePolicy.js'
import { Broadcaster, BroadcastResponse, BroadcastFailure } from './Broadcaster.js'
import MerklePath from './MerklePath.js'
import Spend from '../script/Spend.js'
Expand Down Expand Up @@ -411,7 +411,7 @@ export default class Transaction {

/**
* Computes fees prior to signing.
* If no fee model is provided, uses a SatoshisPerKilobyte fee model that pays 1 sat/kb.
* If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
* If fee is a number, the transaction uses that value as fee.
*
* @param modelOrFee - The initialized fee model to use or fixed fee for the transaction
Expand All @@ -420,7 +420,7 @@ export default class Transaction {
*
*/
async fee (
modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1),
modelOrFee: FeeModel | number = LivePolicy.getInstance(),
changeDistribution: 'equal' | 'random' = 'equal'
): Promise<void> {
this.cachedHash = undefined
Expand Down Expand Up @@ -774,7 +774,7 @@ export default class Transaction {
*
* @returns Whether the transaction is valid according to the rules of SPV.
*
* @example tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
* @example tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
*/
async verify (
chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(),
Expand Down
97 changes: 97 additions & 0 deletions src/transaction/fee-models/LivePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import SatoshisPerKilobyte from './SatoshisPerKilobyte.js'
import Transaction from '../Transaction.js'

/**
* Represents a live fee policy that fetches current rates from ARC GorillaPool.
* Extends SatoshisPerKilobyte to reuse transaction size calculation logic.
*/
export default class LivePolicy extends SatoshisPerKilobyte {
private static readonly ARC_POLICY_URL = 'https://arc.gorillapool.io/v1/policy'
private static instance: LivePolicy | null = null
private cachedRate: number | null = null
private cacheTimestamp: number = 0
private readonly cacheValidityMs: number

/**
* Constructs an instance of the live policy fee model.
*
* @param {number} cacheValidityMs - How long to cache the fee rate in milliseconds (default: 5 minutes)
*/
constructor(cacheValidityMs: number = 5 * 60 * 1000) {
super(100) // Initialize with dummy value, will be overridden by fetchFeeRate
this.cacheValidityMs = cacheValidityMs
}

/**
* Gets the singleton instance of LivePolicy to ensure cache sharing across the application.
*
* @param {number} cacheValidityMs - How long to cache the fee rate in milliseconds (default: 5 minutes)
* @returns The singleton LivePolicy instance
*/
static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy {
if (!LivePolicy.instance) {
LivePolicy.instance = new LivePolicy(cacheValidityMs)
}
return LivePolicy.instance
}

/**
* Fetches the current fee rate from ARC GorillaPool API.
*
* @returns The current satoshis per kilobyte rate
*/
private async fetchFeeRate(): Promise<number> {
const now = Date.now()

// Return cached rate if still valid
if (this.cachedRate !== null && (now - this.cacheTimestamp) < this.cacheValidityMs) {
return this.cachedRate
}

try {
const response = await fetch(LivePolicy.ARC_POLICY_URL)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

const response_data = await response.json()

if (!response_data.policy?.miningFee || typeof response_data.policy.miningFee.satoshis !== 'number' || typeof response_data.policy.miningFee.bytes !== 'number') {
throw new Error('Invalid policy response format')
}

// Convert to satoshis per kilobyte
const rate = (response_data.policy.miningFee.satoshis / response_data.policy.miningFee.bytes) * 1000

// Cache the result
this.cachedRate = rate
this.cacheTimestamp = now

return rate
} catch (error) {
// If we have a cached rate, use it as fallback
if (this.cachedRate !== null) {
console.warn('Failed to fetch live fee rate, using cached value:', error)
return this.cachedRate
}

// Otherwise, use a reasonable default (100 sat/kb)
console.warn('Failed to fetch live fee rate, using default 100 sat/kb:', error)
return 100
}
}

/**
* Computes the fee for a given transaction using the current live rate.
* Overrides the parent method to use dynamic rate fetching.
*
* @param tx The transaction for which a fee is to be computed.
* @returns The fee in satoshis for the transaction.
*/
async computeFee(tx: Transaction): Promise<number> {
const rate = await this.fetchFeeRate()
// Update the value property so parent's computeFee uses the live rate
this.value = rate
return super.computeFee(tx)
}
}
Loading