ens-budget 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Namepass LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,237 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # ens-budget
4
+
5
+ **Renew ENS names by budget.**
6
+
7
+ Give it a label and a stablecoin amount. It returns the longest renewal that amount buys under ENS v2 pricing, checked against ENS's own quote, and can send the renewal for you.
8
+
9
+ [![npm](https://img.shields.io/npm/v/ens-budget?style=flat-square&color=0080BC)](https://www.npmjs.com/package/ens-budget)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/wildcardlabs/ens-budget/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/wildcardlabs/ens-budget/actions/workflows/ci.yml)
11
+ [![min+gzip](https://img.shields.io/bundlephobia/minzip/ens-budget?style=flat-square&label=min%2Bgzip)](https://bundlephobia.com/package/ens-budget)
12
+ [![types](https://img.shields.io/npm/types/ens-budget?style=flat-square)](https://www.npmjs.com/package/ens-budget)
13
+ [![viem](https://img.shields.io/npm/dependency-version/ens-budget/peer/viem?style=flat-square)](https://viem.sh)
14
+ [![license](https://img.shields.io/npm/l/ens-budget?style=flat-square)](./LICENSE)
15
+
16
+ [Install](#install) · [Quick start](#quick-start) · [How it works](#how-it-works) · [Recipes](#recipes) · [API](#api) · [Networks](#networks)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Features
23
+
24
+ - **Exact.** Prices match ENS's `getRenewPrice` to the smallest token unit, including multi-year discounts.
25
+ - **Optimal.** Returns the longest affordable term even where a longer term is cheaper.
26
+ - **Complete.** Picks the right renewer, handles the token approval, and sends the renewal.
27
+ - **Composable.** Returns plain calls for EIP-5792 batching, smart accounts and relayers.
28
+ - **Small.** About 3 kB min+gzip. viem is the only dependency, as a peer.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ npm install ens-budget viem
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```ts
39
+ import { createPublicClient, http } from 'viem'
40
+ import { mainnet } from 'viem/chains'
41
+ import { addresses, quoteRenewal } from 'ens-budget'
42
+
43
+ const client = createPublicClient({ chain: mainnet, transport: http() })
44
+
45
+ const quote = await quoteRenewal(client, {
46
+ label: 'namepass', // namepass.eth
47
+ budget: 25_000000n, // $25 in USDC (6 decimals)
48
+ token: addresses[1].usdc,
49
+ })
50
+
51
+ quote.duration // 143345079n seconds, about 4 years 6 months
52
+ quote.amount // 25000000n, the exact cost
53
+ quote.tier // { duration: 94608000n, discountBps: 3125 } 3-year rate, 31.25% off
54
+ quote.nextTier // { duration: 189216000n, extraAmount: 2000071n, … } 6 years for $2.00 more
55
+ ```
56
+
57
+ To renew, pass a wallet client:
58
+
59
+ ```ts
60
+ import { renewWithBudget } from 'ens-budget'
61
+
62
+ const { hash, quote } = await renewWithBudget(walletClient, {
63
+ label: 'namepass',
64
+ budget: 25_000000n,
65
+ token: addresses[1].usdc,
66
+ })
67
+ ```
68
+
69
+ ## How it works
70
+
71
+ ENS v2 prices renewals in USD per second, by label length, and discounts the whole term once it passes 2, 3 or 6 years. Because the discount applies to every second of the term, price is not monotonic in duration:
72
+
73
+ | `namepass.eth` | Price |
74
+ | --- | --- |
75
+ | 1 year | $8.00 |
76
+ | 5 years | $27.50 |
77
+ | 6 years | **$27.00** |
78
+
79
+ Dividing a budget by an annual rate gets this wrong. `quoteRenewal` instead:
80
+
81
+ 1. Asks both ENS renewers `isRenewable(label)` and uses the one that says yes.
82
+ 2. Reads the live rates, discount tiers and token exchange rate from the renewer's price oracle.
83
+ 3. Solves for the longest duration whose exact price fits the budget, trying discount tiers from best to worst.
84
+ 4. Confirms with `getRenewPrice` that ENS charges exactly the solved amount, and more than the budget for one second longer.
85
+
86
+ ### Approvals
87
+
88
+ Renewals are paid in an ERC-20 token, so the renewer needs an allowance. `renewWithBudget` checks the current allowance and, only if it is short, approves **exactly** `quote.amount` and waits for confirmation before renewing. It never approves an unlimited amount or the full budget, so a price change between quote and send makes the renewal revert instead of overspending.
89
+
90
+ ## Recipes
91
+
92
+ <details>
93
+ <summary><b>Batch approve and renew into one prompt (EIP-5792)</b></summary>
94
+
95
+ ```ts
96
+ import { prepareRenewal, quoteRenewal } from 'ens-budget'
97
+
98
+ const quote = await quoteRenewal(client, { label, budget, token })
99
+ const calls = await prepareRenewal(client, quote, { account: wallet.account.address })
100
+ // [approve, renew], or [renew] when the allowance already covers it
101
+
102
+ await wallet.sendCalls({ calls })
103
+ ```
104
+
105
+ The same `{ to, data }` calls work with Safe transactions, bundlers and relayers.
106
+
107
+ </details>
108
+
109
+ <details>
110
+ <summary><b>Run on a server</b></summary>
111
+
112
+ ```ts
113
+ import { createWalletClient, http, nonceManager, publicActions } from 'viem'
114
+ import { privateKeyToAccount } from 'viem/accounts'
115
+
116
+ const account = privateKeyToAccount(process.env.PRIVATE_KEY, { nonceManager })
117
+ const wallet = createWalletClient({ account, chain: mainnet, transport: http(RPC_URL) }).extend(publicActions)
118
+
119
+ await renewWithBudget(wallet, { label, budget, token })
120
+ ```
121
+
122
+ Exact approvals mean one `approve` transaction per renewal. For frequent renewals, set a standing allowance on the renewer yourself; `prepareRenewal` skips the approval whenever the allowance covers the price.
123
+
124
+ </details>
125
+
126
+ <details>
127
+ <summary><b>Price locally for live UIs</b></summary>
128
+
129
+ Read pricing once, then solve without network calls, for example on every slider move:
130
+
131
+ ```ts
132
+ import { getPricing, priceOf, solve } from 'ens-budget'
133
+
134
+ const pricing = await getPricing(client, { token })
135
+
136
+ solve(pricing, { label: 'namepass', budget: 25_000000n }) // { duration, amount, tier, nextTier }
137
+ priceOf(pricing, { label: 'namepass', duration: 31536000n }) // 8000021n
138
+ ```
139
+
140
+ </details>
141
+
142
+ <details>
143
+ <summary><b>Normalize user input</b></summary>
144
+
145
+ Labels are used exactly as given, as in ensjs, so non-normalized legacy names stay renewable. Normalize anything a user types:
146
+
147
+ ```ts
148
+ import { normalize } from 'viem/ens'
149
+
150
+ const label = normalize(input).replace(/\.eth$/, '')
151
+ ```
152
+
153
+ Length is counted in Unicode code points of the label you pass, as ENS counts it. `❤️❤️❤️` with variation selectors is 6 code points and priced as a 5+ character name; `❤❤❤` is 3.
154
+
155
+ </details>
156
+
157
+ ## API
158
+
159
+ ### Network
160
+
161
+ | Function | Returns |
162
+ | --- | --- |
163
+ | `quoteRenewal(client, { label, budget, token, renewers? })` | `Quote`: the longest renewal for the budget, confirmed with ENS. |
164
+ | `renewWithBudget(walletClient, { label, budget, token, referrer?, renewers? })` | `{ hash, quote }` after approving if needed and renewing. |
165
+ | `prepareRenewal(client, quote, { account, referrer? })` | `Call[]`: `[approve?, renew]` as `{ to, data }`. |
166
+ | `getPricing(client, { token, renewer?, renewers? })` | `Pricing`: oracle rates, discount tiers and token ratio. |
167
+ | `getRenewer(client, { label, renewers? })` | The renewer address for a label, or `null`. |
168
+
169
+ ### Pure
170
+
171
+ | Function | Returns |
172
+ | --- | --- |
173
+ | `solve(pricing, { label, budget })` | `{ duration, amount, tier, nextTier }` |
174
+ | `priceOf(pricing, { label, duration })` | Exact cost in token units. |
175
+
176
+ All amounts are `bigint` in the token's smallest unit. All durations are `bigint` seconds.
177
+
178
+ <details>
179
+ <summary><b>Types</b></summary>
180
+
181
+ ```ts
182
+ type Quote = {
183
+ label: string
184
+ renewer: Address // ETHRegistrar or ETHRenewerV1
185
+ token: Address
186
+ oracle: Address
187
+ budget: bigint
188
+ duration: bigint
189
+ amount: bigint // <= budget
190
+ tier: Tier | null // null at full price
191
+ nextTier: NextTier | null
192
+ }
193
+
194
+ type Tier = { duration: bigint; discountBps: number }
195
+
196
+ type NextTier = Tier & {
197
+ amount: bigint // cost of nextTier.duration
198
+ extraAmount: bigint // amount - budget
199
+ extraDuration: bigint // extra time gained by paying amount
200
+ }
201
+ ```
202
+
203
+ </details>
204
+
205
+ ### Errors
206
+
207
+ All errors extend `EnsBudgetError`.
208
+
209
+ | Error | Thrown when |
210
+ | --- | --- |
211
+ | `InvalidLabelError` | The label is empty, over 255 bytes, or a length ENS does not sell. |
212
+ | `NotRenewableError` | Neither renewer will renew the label (unregistered or past grace). |
213
+ | `UnsupportedTokenError` | The price oracle does not accept the token. |
214
+ | `UnsupportedChainError` | No built-in addresses for the chain and no `renewers` given. |
215
+ | `BudgetTooLowError` | The budget buys less than the minimum renewal. |
216
+ | `QuoteMismatchError` | The local price disagrees with ENS's quote. The renewal is not sent. |
217
+
218
+ ## Networks
219
+
220
+ | Network | ETHRegistrar (v2 names) | ETHRenewerV1 (unmigrated v1 names) | Tokens |
221
+ | --- | --- | --- | --- |
222
+ | Mainnet | `0xD6D8369e55c94Ae8AFA714E6cF7AB5bE54D5CC81` | `0x56c447F308F463A01121C41E72b0E7e7557fCb68` | USDC, DAI |
223
+ | Sepolia | `0xf633e7FC17e2bbE0D0965D18ec1821dcB754a3d3` | `0xf2ece44980778966b8a0FccB3A9E339440f6e045` | USDC, DAI |
224
+
225
+ Token addresses are exported as `addresses[chainId].usdc` and `.dai`. For forks or other deployments, pass `renewers: { ethRegistrar, ethRenewerV1 }`. The price oracle is always read from the renewer, never hardcoded.
226
+
227
+ ## Development
228
+
229
+ ```sh
230
+ npm test # unit tests, including a brute-force optimality check
231
+ npm run test:live # read-only checks against mainnet (MAINNET_RPC_URL optional)
232
+ npm run test:fork # real renewals on an anvil mainnet fork; requires Foundry
233
+ ```
234
+
235
+ ## License
236
+
237
+ [MIT](./LICENSE) © Namepass LLC
package/dist/abi.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * ABI fragments, inlined so the package carries only what it calls.
3
+ *
4
+ * `ETHRegistrar` (v2 names) and `ETHRenewerV1` (unmigrated v1 names) both
5
+ * implement `IETHRenewer`, so one fragment drives either.
6
+ */
7
+ export declare const renewerAbi: readonly [{
8
+ readonly type: "function";
9
+ readonly name: "renew";
10
+ readonly stateMutability: "nonpayable";
11
+ readonly inputs: readonly [{
12
+ readonly name: "rd";
13
+ readonly type: "tuple";
14
+ readonly components: readonly [{
15
+ readonly name: "label";
16
+ readonly type: "string";
17
+ }, {
18
+ readonly name: "duration";
19
+ readonly type: "uint64";
20
+ }, {
21
+ readonly name: "referrer";
22
+ readonly type: "bytes32";
23
+ }];
24
+ }, {
25
+ readonly name: "paymentToken";
26
+ readonly type: "address";
27
+ }];
28
+ readonly outputs: readonly [];
29
+ }, {
30
+ readonly type: "function";
31
+ readonly name: "isRenewable";
32
+ readonly stateMutability: "view";
33
+ readonly inputs: readonly [{
34
+ readonly name: "label";
35
+ readonly type: "string";
36
+ }];
37
+ readonly outputs: readonly [{
38
+ readonly name: "";
39
+ readonly type: "bool";
40
+ }];
41
+ }, {
42
+ readonly type: "function";
43
+ readonly name: "getRenewPrice";
44
+ readonly stateMutability: "view";
45
+ readonly inputs: readonly [{
46
+ readonly name: "label";
47
+ readonly type: "string";
48
+ }, {
49
+ readonly name: "duration";
50
+ readonly type: "uint64";
51
+ }, {
52
+ readonly name: "paymentToken";
53
+ readonly type: "address";
54
+ }];
55
+ readonly outputs: readonly [{
56
+ readonly name: "";
57
+ readonly type: "uint256";
58
+ }];
59
+ }, {
60
+ readonly type: "function";
61
+ readonly name: "rentPriceOracle";
62
+ readonly stateMutability: "view";
63
+ readonly inputs: readonly [];
64
+ readonly outputs: readonly [{
65
+ readonly name: "";
66
+ readonly type: "address";
67
+ }];
68
+ }, {
69
+ readonly type: "function";
70
+ readonly name: "MIN_RENEW_DURATION";
71
+ readonly stateMutability: "view";
72
+ readonly inputs: readonly [];
73
+ readonly outputs: readonly [{
74
+ readonly name: "";
75
+ readonly type: "uint64";
76
+ }];
77
+ }];
78
+ export declare const oracleAbi: readonly [{
79
+ readonly type: "function";
80
+ readonly name: "DISCOUNT_DENOMINATOR";
81
+ readonly stateMutability: "view";
82
+ readonly inputs: readonly [];
83
+ readonly outputs: readonly [{
84
+ readonly name: "";
85
+ readonly type: "uint128";
86
+ }];
87
+ }, {
88
+ readonly type: "function";
89
+ readonly name: "getBaseRates";
90
+ readonly stateMutability: "view";
91
+ readonly inputs: readonly [];
92
+ readonly outputs: readonly [{
93
+ readonly name: "";
94
+ readonly type: "uint256[]";
95
+ }];
96
+ }, {
97
+ readonly type: "function";
98
+ readonly name: "getDiscountPoints";
99
+ readonly stateMutability: "view";
100
+ readonly inputs: readonly [];
101
+ readonly outputs: readonly [{
102
+ readonly name: "";
103
+ readonly type: "tuple[]";
104
+ readonly components: readonly [{
105
+ readonly name: "duration";
106
+ readonly type: "uint64";
107
+ }, {
108
+ readonly name: "numer";
109
+ readonly type: "uint128";
110
+ }];
111
+ }];
112
+ }, {
113
+ readonly type: "function";
114
+ readonly name: "getPaymentTokenRatio";
115
+ readonly stateMutability: "view";
116
+ readonly inputs: readonly [{
117
+ readonly name: "paymentToken";
118
+ readonly type: "address";
119
+ }];
120
+ readonly outputs: readonly [{
121
+ readonly name: "numer";
122
+ readonly type: "uint128";
123
+ }, {
124
+ readonly name: "denom";
125
+ readonly type: "uint128";
126
+ }];
127
+ }];
128
+ export declare const erc20Abi: readonly [{
129
+ readonly type: "function";
130
+ readonly name: "allowance";
131
+ readonly stateMutability: "view";
132
+ readonly inputs: readonly [{
133
+ readonly name: "owner";
134
+ readonly type: "address";
135
+ }, {
136
+ readonly name: "spender";
137
+ readonly type: "address";
138
+ }];
139
+ readonly outputs: readonly [{
140
+ readonly name: "";
141
+ readonly type: "uint256";
142
+ }];
143
+ }, {
144
+ readonly type: "function";
145
+ readonly name: "approve";
146
+ readonly stateMutability: "nonpayable";
147
+ readonly inputs: readonly [{
148
+ readonly name: "spender";
149
+ readonly type: "address";
150
+ }, {
151
+ readonly name: "amount";
152
+ readonly type: "uint256";
153
+ }];
154
+ readonly outputs: readonly [{
155
+ readonly name: "";
156
+ readonly type: "bool";
157
+ }];
158
+ }];
package/dist/abi.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * ABI fragments, inlined so the package carries only what it calls.
3
+ *
4
+ * `ETHRegistrar` (v2 names) and `ETHRenewerV1` (unmigrated v1 names) both
5
+ * implement `IETHRenewer`, so one fragment drives either.
6
+ */
7
+ export const renewerAbi = [
8
+ {
9
+ type: 'function',
10
+ name: 'renew',
11
+ stateMutability: 'nonpayable',
12
+ inputs: [
13
+ {
14
+ name: 'rd',
15
+ type: 'tuple',
16
+ components: [
17
+ { name: 'label', type: 'string' },
18
+ { name: 'duration', type: 'uint64' },
19
+ { name: 'referrer', type: 'bytes32' },
20
+ ],
21
+ },
22
+ { name: 'paymentToken', type: 'address' },
23
+ ],
24
+ outputs: [],
25
+ },
26
+ {
27
+ type: 'function',
28
+ name: 'isRenewable',
29
+ stateMutability: 'view',
30
+ inputs: [{ name: 'label', type: 'string' }],
31
+ outputs: [{ name: '', type: 'bool' }],
32
+ },
33
+ {
34
+ type: 'function',
35
+ name: 'getRenewPrice',
36
+ stateMutability: 'view',
37
+ inputs: [
38
+ { name: 'label', type: 'string' },
39
+ { name: 'duration', type: 'uint64' },
40
+ { name: 'paymentToken', type: 'address' },
41
+ ],
42
+ outputs: [{ name: '', type: 'uint256' }],
43
+ },
44
+ {
45
+ type: 'function',
46
+ name: 'rentPriceOracle',
47
+ stateMutability: 'view',
48
+ inputs: [],
49
+ outputs: [{ name: '', type: 'address' }],
50
+ },
51
+ {
52
+ type: 'function',
53
+ name: 'MIN_RENEW_DURATION',
54
+ stateMutability: 'view',
55
+ inputs: [],
56
+ outputs: [{ name: '', type: 'uint64' }],
57
+ },
58
+ ];
59
+ export const oracleAbi = [
60
+ {
61
+ type: 'function',
62
+ name: 'DISCOUNT_DENOMINATOR',
63
+ stateMutability: 'view',
64
+ inputs: [],
65
+ outputs: [{ name: '', type: 'uint128' }],
66
+ },
67
+ {
68
+ type: 'function',
69
+ name: 'getBaseRates',
70
+ stateMutability: 'view',
71
+ inputs: [],
72
+ outputs: [{ name: '', type: 'uint256[]' }],
73
+ },
74
+ {
75
+ type: 'function',
76
+ name: 'getDiscountPoints',
77
+ stateMutability: 'view',
78
+ inputs: [],
79
+ outputs: [
80
+ {
81
+ name: '',
82
+ type: 'tuple[]',
83
+ components: [
84
+ { name: 'duration', type: 'uint64' },
85
+ { name: 'numer', type: 'uint128' },
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ {
91
+ type: 'function',
92
+ name: 'getPaymentTokenRatio',
93
+ stateMutability: 'view',
94
+ inputs: [{ name: 'paymentToken', type: 'address' }],
95
+ outputs: [
96
+ { name: 'numer', type: 'uint128' },
97
+ { name: 'denom', type: 'uint128' },
98
+ ],
99
+ },
100
+ ];
101
+ export const erc20Abi = [
102
+ {
103
+ type: 'function',
104
+ name: 'allowance',
105
+ stateMutability: 'view',
106
+ inputs: [
107
+ { name: 'owner', type: 'address' },
108
+ { name: 'spender', type: 'address' },
109
+ ],
110
+ outputs: [{ name: '', type: 'uint256' }],
111
+ },
112
+ {
113
+ type: 'function',
114
+ name: 'approve',
115
+ stateMutability: 'nonpayable',
116
+ inputs: [
117
+ { name: 'spender', type: 'address' },
118
+ { name: 'amount', type: 'uint256' },
119
+ ],
120
+ outputs: [{ name: '', type: 'bool' }],
121
+ },
122
+ ];
123
+ //# sourceMappingURL=abi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abi.js","sourceRoot":"","sources":["../src/abi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { Address } from 'viem';
2
+ export type Renewers = {
3
+ /** v2 `ETHRegistrar`: renews names registered on, or migrated to, v2. */
4
+ ethRegistrar: Address;
5
+ /** `ETHRenewerV1`: renews unmigrated legacy (v1) names. */
6
+ ethRenewerV1: Address;
7
+ };
8
+ /**
9
+ * Deployed ENS v2 renewers and the stablecoins ENS prices, per chain.
10
+ *
11
+ * Sourced from `@ensdomains/ensjs` and checked on-chain: both renewers expose
12
+ * `renew((string,uint64,bytes32),address)` and neither is an upgradeable
13
+ * proxy. The rent price oracle is *not* listed here; renewers can point at a
14
+ * new one, so it is read from `rentPriceOracle()` every time.
15
+ */
16
+ export declare const addresses: {
17
+ readonly 1: {
18
+ readonly ethRegistrar: "0xD6D8369e55c94Ae8AFA714E6cF7AB5bE54D5CC81";
19
+ readonly ethRenewerV1: "0x56c447F308F463A01121C41E72b0E7e7557fCb68";
20
+ readonly usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
21
+ readonly dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F";
22
+ };
23
+ readonly 11155111: {
24
+ readonly ethRegistrar: "0xf633e7FC17e2bbE0D0965D18ec1821dcB754a3d3";
25
+ readonly ethRenewerV1: "0xf2ece44980778966b8a0FccB3A9E339440f6e045";
26
+ readonly usdc: "0x240b0316Df57887DBBE58b586508b19e633a14aa";
27
+ readonly dai: "0xF6faC8A58a0BE13b9197f27C41B73162Fe32572b";
28
+ };
29
+ };
30
+ export declare function renewersFor(chainId: number | undefined, override?: Renewers): Renewers;
@@ -0,0 +1,32 @@
1
+ import { UnsupportedChainError } from './errors.js';
2
+ /**
3
+ * Deployed ENS v2 renewers and the stablecoins ENS prices, per chain.
4
+ *
5
+ * Sourced from `@ensdomains/ensjs` and checked on-chain: both renewers expose
6
+ * `renew((string,uint64,bytes32),address)` and neither is an upgradeable
7
+ * proxy. The rent price oracle is *not* listed here; renewers can point at a
8
+ * new one, so it is read from `rentPriceOracle()` every time.
9
+ */
10
+ export const addresses = {
11
+ 1: {
12
+ ethRegistrar: '0xD6D8369e55c94Ae8AFA714E6cF7AB5bE54D5CC81',
13
+ ethRenewerV1: '0x56c447F308F463A01121C41E72b0E7e7557fCb68',
14
+ usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
15
+ dai: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
16
+ },
17
+ 11155111: {
18
+ ethRegistrar: '0xf633e7FC17e2bbE0D0965D18ec1821dcB754a3d3',
19
+ ethRenewerV1: '0xf2ece44980778966b8a0FccB3A9E339440f6e045',
20
+ usdc: '0x240b0316Df57887DBBE58b586508b19e633a14aa',
21
+ dai: '0xF6faC8A58a0BE13b9197f27C41B73162Fe32572b',
22
+ },
23
+ };
24
+ export function renewersFor(chainId, override) {
25
+ if (override)
26
+ return override;
27
+ const known = chainId === undefined ? undefined : addresses[chainId];
28
+ if (!known)
29
+ throw new UnsupportedChainError(chainId);
30
+ return known;
31
+ }
32
+ //# sourceMappingURL=addresses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addresses.js","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AASnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,CAAC,EAAE;QACD,YAAY,EAAE,4CAA4C;QAC1D,YAAY,EAAE,4CAA4C;QAC1D,IAAI,EAAE,4CAA4C;QAClD,GAAG,EAAE,4CAA4C;KAClD;IACD,QAAQ,EAAE;QACR,YAAY,EAAE,4CAA4C;QAC1D,YAAY,EAAE,4CAA4C;QAC1D,IAAI,EAAE,4CAA4C;QAClD,GAAG,EAAE,4CAA4C;KAClD;CAC4E,CAAA;AAE/E,MAAM,UAAU,WAAW,CAAC,OAA2B,EAAE,QAAmB;IAC1E,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,MAAM,KAAK,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,OAAiC,CAAC,CAAA;IAC9F,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,qBAAqB,CAAC,OAAO,CAAC,CAAA;IACpD,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -0,0 +1,43 @@
1
+ import type { Address } from 'viem';
2
+ export declare class EnsBudgetError extends Error {
3
+ name: string;
4
+ }
5
+ /** Empty, over 255 bytes, or a length ENS doesn't sell (e.g. 1–2 characters). */
6
+ export declare class InvalidLabelError extends EnsBudgetError {
7
+ readonly label: string;
8
+ name: string;
9
+ constructor(label: string, reason: string);
10
+ }
11
+ /** Neither renewer will renew the label right now (unregistered, or past grace). */
12
+ export declare class NotRenewableError extends EnsBudgetError {
13
+ readonly label: string;
14
+ name: string;
15
+ constructor(label: string);
16
+ }
17
+ /** The rent price oracle has no exchange rate for this token. */
18
+ export declare class UnsupportedTokenError extends EnsBudgetError {
19
+ readonly token: Address;
20
+ name: string;
21
+ constructor(token: Address);
22
+ }
23
+ /** No built-in renewer addresses for this chain; pass `renewers`. */
24
+ export declare class UnsupportedChainError extends EnsBudgetError {
25
+ readonly chainId: number | undefined;
26
+ name: string;
27
+ constructor(chainId: number | undefined);
28
+ }
29
+ /** The budget buys less than the renewer's minimum duration. */
30
+ export declare class BudgetTooLowError extends EnsBudgetError {
31
+ readonly budget: bigint;
32
+ readonly duration: bigint;
33
+ readonly minDuration: bigint;
34
+ name: string;
35
+ constructor(budget: bigint, duration: bigint, minDuration: bigint);
36
+ }
37
+ /** The local solve disagrees with ENS's own `getRenewPrice`. Should never happen. */
38
+ export declare class QuoteMismatchError extends EnsBudgetError {
39
+ readonly expected: bigint;
40
+ readonly actual: bigint;
41
+ name: string;
42
+ constructor(expected: bigint, actual: bigint);
43
+ }