Skip to content

Commit a5689c5

Browse files
authored
Merge pull request #28 from thenamespace/feat/set-default-evm-address
feat(offchain-manager): implement setDefaultEvmAddress method
2 parents ddbbd59 + d384330 commit a5689c5

File tree

8 files changed

+165
-31
lines changed

8 files changed

+165
-31
lines changed

.github/workflows/publish-release.yaml

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ on:
2121
default: false
2222
changelog:
2323
type: string
24-
description: "Changelog (required if publishing, markdown format, do NOT include version or date header. Use sections like ### Added, ### Changed, etc.)"
24+
description: "Changelog (required if publishing, markdown format, do NOT include version or date header. Use sections like ### Added, ### Changed, etc. Each section should be on a new line, and bullet points should start with -)"
2525
default: ""
2626
discord_notify:
2727
type: boolean
@@ -159,37 +159,36 @@ jobs:
159159
echo -e "# Changelog\n" > "$FILE"
160160
fi
161161
162-
# Find the line number of the "# Changelog" header
163-
HEADER_LINE=$(grep -n "^# Changelog" "$FILE" | cut -d: -f1)
164-
if [ -z "$HEADER_LINE" ]; then
165-
# If not found, just prepend
166-
HEADER_LINE=0
167-
fi
162+
# Find the insertion point - look for the first version entry (## [version])
163+
# This ensures we insert after the intro section but before any existing versions
164+
FIRST_VERSION_LINE=$(grep -n "^## \[" "$FILE" | head -1 | cut -d: -f1)
168165
169-
# Find the line number after the intro section (after the first blank line following '# Changelog')
170-
INTRO_END_LINE=$(awk '/^# Changelog/{flag=1; next} flag && /^$/{print NR; exit}' "$FILE")
171-
if [ -z "$INTRO_END_LINE" ]; then
172-
# Fallback: if not found, use header line
173-
HEADER_LINE=$(grep -n "^# Changelog" "$FILE" | cut -d: -f1)
174-
if [ -z "$HEADER_LINE" ]; then
175-
HEADER_LINE=0
176-
fi
177-
INTRO_END_LINE=$((HEADER_LINE + 1))
166+
if [ -z "$FIRST_VERSION_LINE" ]; then
167+
# No existing versions found, append to end of file
168+
INSERTION_LINE=$(wc -l < "$FILE")
169+
else
170+
# Insert before the first version entry
171+
INSERTION_LINE=$((FIRST_VERSION_LINE - 1))
178172
fi
179173
180174
# Process the changelog input to ensure proper formatting
181-
# Handle both multi-line and single-line input from GitHub UI
182-
PRETTY_CHANGELOG=$(echo "$CHANGELOG" | sed -E 's/\\n/\n/g' | sed -E 's/\\1/\n/g' | sed -E 's/^[[:space:]]*//' | sed -E 's/[[:space:]]*$//')
183-
184-
# Write up to and including the intro
185-
if [ "$INTRO_END_LINE" -gt 0 ]; then
186-
head -n "$INTRO_END_LINE" "$FILE" > "$TEMP_FILE"
175+
# Python3 is pre-installed on ubuntu-latest runners (free, no extra cost)
176+
PRETTY_CHANGELOG=$(python3 scripts/format-changelog.py "$CHANGELOG")
177+
178+
# Write the file up to insertion point
179+
if [ "$INSERTION_LINE" -gt 0 ]; then
180+
head -n "$INSERTION_LINE" "$FILE" > "$TEMP_FILE"
181+
else
182+
# If no insertion point found, start with the header
183+
echo -e "# Changelog\n" > "$TEMP_FILE"
187184
fi
188-
# Add the new entry
185+
186+
# Add the new entry with proper formatting
189187
echo -e "\n${HEADER}\n\n${PRETTY_CHANGELOG}\n" >> "$TEMP_FILE"
190-
# Add the rest of the file
191-
if [ "$INTRO_END_LINE" -gt 0 ]; then
192-
tail -n "+$((INTRO_END_LINE + 1))" "$FILE" >> "$TEMP_FILE"
188+
189+
# Add the rest of the file (existing versions)
190+
if [ "$INSERTION_LINE" -gt 0 ] && [ "$INSERTION_LINE" -lt $(wc -l < "$FILE") ]; then
191+
tail -n "+$((INSERTION_LINE + 1))" "$FILE" >> "$TEMP_FILE"
193192
fi
194193
mv "$TEMP_FILE" "$FILE"
195194

changelog/offchain-manager-changelog.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Changelog
22

3-
4-
## [1.0.7] - 2025-10-16
5-
6-
### Added - **Default EVM chain **: Supported default chain introduced in ENSIP 19 with chainId = 0
7-
83
All notable changes to the `@thenamespace/offchain-manager` package will be documented in this file.
94

105
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
116
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
127

8+
## [1.0.7] - 2025-10-16
9+
10+
### Added
11+
12+
- **Default EVM chain**: Supported default chain introduced in ENSIP 19 with chainId = 0
13+
1314
## [1.0.6] - 2025-10-16
1415

1516
### Added

packages/offchain-manager/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ await client.addAddressRecord(
158158
await client.deleteAddressRecord("sub.example.eth", ChainName.Base);
159159
```
160160

161+
#### Set Default EVM Address
162+
163+
```typescript
164+
// Sets the same EVM address for all EVM-compatible chains (Ethereum, Arbitrum, Optimism, Base, Polygon, BSC, Avalanche, Gnosis, zkSync, Linea, Scroll, Unichain, Berachain, WorldChain, Zora, Celo, and Monad)
165+
await client.setDefaultEvmAddress("sub.example.eth", "0xYourEthereumAddress");
166+
```
167+
161168
#### Add a Text Record
162169

163170
```typescript

packages/offchain-manager/examples/basic-usage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ async function basicExample() {
6161
console.log('📋 Found existing subname:', existing?.fullName);
6262
}
6363

64+
// Set default EVM address for all EVM chains
65+
await client.setDefaultEvmAddress('alice.happ1.eth', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
66+
console.log('✅ Set default EVM address for all EVM chains to alice.happ1.eth');
67+
6468
// Get subnames for a domain
6569
const subnames = await client.getFilteredSubnames({
6670
parentName: 'happ1.eth',

packages/offchain-manager/src/dto/chains.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,49 +79,61 @@ export interface ChainMetadata {
7979
label: string;
8080
/** SLIP-0044 coin type used for ENS address records */
8181
coin: number;
82+
/** Whether this chain is EVM-compatible and should mirror default EVM address */
83+
evm?: boolean;
8284
}
8385

8486

8587
export const chainMetadata: Record<ChainName, ChainMetadata> = {
8688
eth: {
8789
label: "Ethereum",
8890
coin: 60,
91+
evm: true,
8992
},
9093
default: {
9194
label: "Default",
9295
coin: 2147483648,
96+
evm: true,
9397
},
9498
base: {
9599
label: "Base",
96100
coin: 8453,
101+
evm: true,
97102
},
98103
op: {
99104
label: "Optimism",
100105
coin: 10,
106+
evm: true,
101107
},
102108
arb: {
103109
label: "Arbitrum",
104110
coin: 42161,
111+
evm: true,
105112
},
106113
bsc: {
107114
label: "BNB",
108115
coin: 56,
116+
evm: true,
109117
},
110118
polygon: {
111119
label: "Polygon",
112120
coin: 137,
121+
evm: true,
113122
},
114123
avax: {
115124
label: "Avax",
116125
coin: 43114,
126+
evm: true,
117127
},
118128
gnosis: {
119129
label: "Gnosis",
120130
coin: 100,
131+
evm: true,
121132
},
122133
zksync: {
123134
label: "ZkSync",
124135
coin: 324,
136+
evm: true,
125137
},
126138
starknet: {
127139
label: "Starknet",
@@ -146,10 +158,12 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
146158
linea: {
147159
label: "Linea",
148160
coin: 59144,
161+
evm: true,
149162
},
150163
scroll: {
151164
label: "Scroll",
152165
coin: 534352,
166+
evm: true,
153167
},
154168
sui: {
155169
label: "Sui",
@@ -158,22 +172,27 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
158172
unichain: {
159173
label: "Unichain",
160174
coin: 130,
175+
evm: true,
161176
},
162177
berachain: {
163178
label: "Berachain",
164179
coin: 80094,
180+
evm: true,
165181
},
166182
world_chain: {
167183
label: "WorldChain",
168184
coin: 480,
185+
evm: true,
169186
},
170187
zora: {
171188
label: "Zora",
172189
coin: 7777777,
190+
evm: true,
173191
},
174192
celo: {
175193
label: "Celo",
176194
coin: 42220,
195+
evm: true,
177196
},
178197
aptos: {
179198
label: "Aptos",
@@ -186,6 +205,7 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
186205
monad: {
187206
label: "Monad",
188207
coin: 10143,
208+
evm: true,
189209
},
190210
};
191211

packages/offchain-manager/src/offchain-client/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
22
import { SubnameDTO } from "../dto/subname.dto";
33
import {
44
_addAddressRecord,
5+
_setDefaultEthereumAddress,
56
_addDataRecord,
67
_addTextRecord,
78
_createSubname,
@@ -185,6 +186,19 @@ export interface OffchainClient {
185186
*/
186187
deleteAddressRecord(subname: string, chain: ChainName): Promise<void>;
187188

189+
/**
190+
* Set a default EVM address for all EVM-compatible chains to a subname.
191+
* This sets the same address for Ethereum, Arbitrum, Optimism, Base, Polygon, BSC, Avalanche, Gnosis, zkSync, Linea, Scroll, Unichain, Berachain, WorldChain, Zora, Celo, and Monad.
192+
* @param subname - Full subname (e.g., 'alice.example.eth')
193+
* @param value - EVM wallet address to set as default for all supported EVM chains
194+
* @throws {ValidationError} When address format is invalid
195+
* @example
196+
* ```typescript
197+
* await client.setDefaultEvmAddress('alice.example.eth', '0x...');
198+
* ```
199+
*/
200+
setDefaultEvmAddress(subname: string, value: string): Promise<void>;
201+
188202
/**
189203
* Add a text record to a subname.
190204
* @param subname - Full subname (e.g., 'alice.example.eth')
@@ -391,6 +405,18 @@ class HttpOffchainClient implements OffchainClient {
391405
);
392406
}
393407

408+
public async setDefaultEvmAddress(
409+
subname: string,
410+
value: string
411+
): Promise<void> {
412+
await _setDefaultEthereumAddress(
413+
this.HTTP,
414+
this.fetchApiKeyForName(subname),
415+
subname,
416+
value
417+
);
418+
}
419+
394420
public async getSingleSubname(
395421
fullSubname: string
396422
): Promise<SubnameDTO | null> {

packages/offchain-manager/src/offchain-client/private-actions.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
subnameResponseToRequest,
88
} from "./utils";
99
import { CreateSubnameRequest } from "../dto/create-subname-request.dto";
10+
import { chainMetadata } from "../dto/chains";
1011
import { CreateSubnameRequest_Internal } from "../dto/internal-types";
1112
import { _getSingleSubname } from "./public-actions";
1213
import { UpdateSubnameRequest } from "../dto";
@@ -205,6 +206,35 @@ export const _deleteDataRecord = async (
205206
});
206207
};
207208

209+
export const _setDefaultEthereumAddress = async (
210+
client: AxiosInstance,
211+
apiKey: string,
212+
fullSubname: string,
213+
value: string
214+
) => {
215+
const subname = await _getSingleSubname(client, fullSubname);
216+
217+
const addresses = subname.addresses || {};
218+
219+
// Derive EVM chains from shared chainMetadata (evm=true)
220+
Object.values(chainMetadata)
221+
.filter((meta) => meta.evm)
222+
.forEach((meta) => {
223+
addresses[meta.coin] = value;
224+
});
225+
226+
const _req = subnameResponseToRequest(subname);
227+
228+
const request: CreateSubnameRequest_Internal = {
229+
..._req,
230+
addresses: mapAddrMapToAddressRecords(addresses),
231+
};
232+
233+
return client.post(`/api/v1/subnames`, request, {
234+
headers: createAuthorizationHeaders(apiKey),
235+
});
236+
};
237+
208238
const createAuthorizationHeaders = (apiKey: string) => {
209239
return {
210240
[AUTH_HEADER]: `${apiKey}`,

scripts/format-changelog.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Format changelog input for proper markdown formatting.
4+
This handles the conversion from single-line GitHub input to properly formatted markdown.
5+
"""
6+
7+
import sys
8+
import re
9+
10+
def format_changelog(input_text):
11+
"""Format changelog text with proper markdown structure."""
12+
13+
# Convert escaped newlines to actual newlines
14+
text = input_text.replace('\\n', '\n').replace('\\1', '\n')
15+
16+
# Trim whitespace
17+
text = text.strip()
18+
19+
# Fix section headers that appear mid-text - add newlines before them
20+
# Example: "text ### Added" -> "text\n\n### Added"
21+
text = re.sub(r'([^\n]) (### [A-Za-z]+)', r'\1\n\n\2', text)
22+
23+
# Fix section headers - ensure they have proper newlines after the section name
24+
# Example: "### Added - item" -> "### Added\n- item"
25+
text = re.sub(r'^(### [A-Za-z]+) - ', r'\1\n- ', text, flags=re.MULTILINE)
26+
27+
# Fix bullet points that are inline with text
28+
# Example: "text - item" -> "text\n- item"
29+
text = re.sub(r'([^\n]) - ', r'\1\n- ', text)
30+
31+
# Ensure section headers have blank line after them if followed by content
32+
text = re.sub(r'(^### [A-Za-z]+)\n([^-\n])', r'\1\n\n\2', text, flags=re.MULTILINE)
33+
34+
# Clean up multiple newlines (max 2 consecutive)
35+
text = re.sub(r'\n{3,}', '\n\n', text)
36+
37+
return text
38+
39+
if __name__ == '__main__':
40+
if len(sys.argv) < 2:
41+
print("Usage: format-changelog.py <changelog_input>", file=sys.stderr)
42+
sys.exit(1)
43+
44+
input_text = ' '.join(sys.argv[1:])
45+
formatted = format_changelog(input_text)
46+
print(formatted)
47+

0 commit comments

Comments
 (0)