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
51 changes: 25 additions & 26 deletions .github/workflows/publish-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ on:
default: false
changelog:
type: string
description: "Changelog (required if publishing, markdown format, do NOT include version or date header. Use sections like ### Added, ### Changed, etc.)"
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 -)"
default: ""
discord_notify:
type: boolean
Expand Down Expand Up @@ -159,37 +159,36 @@ jobs:
echo -e "# Changelog\n" > "$FILE"
fi

# Find the line number of the "# Changelog" header
HEADER_LINE=$(grep -n "^# Changelog" "$FILE" | cut -d: -f1)
if [ -z "$HEADER_LINE" ]; then
# If not found, just prepend
HEADER_LINE=0
fi
# Find the insertion point - look for the first version entry (## [version])
# This ensures we insert after the intro section but before any existing versions
FIRST_VERSION_LINE=$(grep -n "^## \[" "$FILE" | head -1 | cut -d: -f1)

# Find the line number after the intro section (after the first blank line following '# Changelog')
INTRO_END_LINE=$(awk '/^# Changelog/{flag=1; next} flag && /^$/{print NR; exit}' "$FILE")
if [ -z "$INTRO_END_LINE" ]; then
# Fallback: if not found, use header line
HEADER_LINE=$(grep -n "^# Changelog" "$FILE" | cut -d: -f1)
if [ -z "$HEADER_LINE" ]; then
HEADER_LINE=0
fi
INTRO_END_LINE=$((HEADER_LINE + 1))
if [ -z "$FIRST_VERSION_LINE" ]; then
# No existing versions found, append to end of file
INSERTION_LINE=$(wc -l < "$FILE")
else
# Insert before the first version entry
INSERTION_LINE=$((FIRST_VERSION_LINE - 1))
fi

# Process the changelog input to ensure proper formatting
# Handle both multi-line and single-line input from GitHub UI
PRETTY_CHANGELOG=$(echo "$CHANGELOG" | sed -E 's/\\n/\n/g' | sed -E 's/\\1/\n/g' | sed -E 's/^[[:space:]]*//' | sed -E 's/[[:space:]]*$//')

# Write up to and including the intro
if [ "$INTRO_END_LINE" -gt 0 ]; then
head -n "$INTRO_END_LINE" "$FILE" > "$TEMP_FILE"
# Python3 is pre-installed on ubuntu-latest runners (free, no extra cost)
PRETTY_CHANGELOG=$(python3 scripts/format-changelog.py "$CHANGELOG")

# Write the file up to insertion point
if [ "$INSERTION_LINE" -gt 0 ]; then
head -n "$INSERTION_LINE" "$FILE" > "$TEMP_FILE"
else
# If no insertion point found, start with the header
echo -e "# Changelog\n" > "$TEMP_FILE"
fi
# Add the new entry

# Add the new entry with proper formatting
echo -e "\n${HEADER}\n\n${PRETTY_CHANGELOG}\n" >> "$TEMP_FILE"
# Add the rest of the file
if [ "$INTRO_END_LINE" -gt 0 ]; then
tail -n "+$((INTRO_END_LINE + 1))" "$FILE" >> "$TEMP_FILE"

# Add the rest of the file (existing versions)
if [ "$INSERTION_LINE" -gt 0 ] && [ "$INSERTION_LINE" -lt $(wc -l < "$FILE") ]; then
tail -n "+$((INSERTION_LINE + 1))" "$FILE" >> "$TEMP_FILE"
fi
mv "$TEMP_FILE" "$FILE"

Expand Down
11 changes: 6 additions & 5 deletions changelog/offchain-manager-changelog.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Changelog


## [1.0.7] - 2025-10-16

### Added - **Default EVM chain **: Supported default chain introduced in ENSIP 19 with chainId = 0

All notable changes to the `@thenamespace/offchain-manager` package will be documented in this file.

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

## [1.0.7] - 2025-10-16

### Added

- **Default EVM chain**: Supported default chain introduced in ENSIP 19 with chainId = 0

## [1.0.6] - 2025-10-16

### Added
Expand Down
7 changes: 7 additions & 0 deletions packages/offchain-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ await client.addAddressRecord(
await client.deleteAddressRecord("sub.example.eth", ChainName.Base);
```

#### Set Default EVM Address

```typescript
// 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)
await client.setDefaultEvmAddress("sub.example.eth", "0xYourEthereumAddress");
```

#### Add a Text Record

```typescript
Expand Down
4 changes: 4 additions & 0 deletions packages/offchain-manager/examples/basic-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ async function basicExample() {
console.log('πŸ“‹ Found existing subname:', existing?.fullName);
}

// Set default EVM address for all EVM chains
await client.setDefaultEvmAddress('alice.happ1.eth', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
console.log('βœ… Set default EVM address for all EVM chains to alice.happ1.eth');

// Get subnames for a domain
const subnames = await client.getFilteredSubnames({
parentName: 'happ1.eth',
Expand Down
20 changes: 20 additions & 0 deletions packages/offchain-manager/src/dto/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,49 +79,61 @@ export interface ChainMetadata {
label: string;
/** SLIP-0044 coin type used for ENS address records */
coin: number;
/** Whether this chain is EVM-compatible and should mirror default EVM address */
evm?: boolean;
}


export const chainMetadata: Record<ChainName, ChainMetadata> = {
eth: {
label: "Ethereum",
coin: 60,
evm: true,
},
default: {
label: "Default",
coin: 2147483648,
evm: true,
},
base: {
label: "Base",
coin: 8453,
evm: true,
},
op: {
label: "Optimism",
coin: 10,
evm: true,
},
arb: {
label: "Arbitrum",
coin: 42161,
evm: true,
},
bsc: {
label: "BNB",
coin: 56,
evm: true,
},
polygon: {
label: "Polygon",
coin: 137,
evm: true,
},
avax: {
label: "Avax",
coin: 43114,
evm: true,
},
gnosis: {
label: "Gnosis",
coin: 100,
evm: true,
},
zksync: {
label: "ZkSync",
coin: 324,
evm: true,
},
starknet: {
label: "Starknet",
Expand All @@ -146,10 +158,12 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
linea: {
label: "Linea",
coin: 59144,
evm: true,
},
scroll: {
label: "Scroll",
coin: 534352,
evm: true,
},
sui: {
label: "Sui",
Expand All @@ -158,22 +172,27 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
unichain: {
label: "Unichain",
coin: 130,
evm: true,
},
berachain: {
label: "Berachain",
coin: 80094,
evm: true,
},
world_chain: {
label: "WorldChain",
coin: 480,
evm: true,
},
zora: {
label: "Zora",
coin: 7777777,
evm: true,
},
celo: {
label: "Celo",
coin: 42220,
evm: true,
},
aptos: {
label: "Aptos",
Expand All @@ -186,6 +205,7 @@ export const chainMetadata: Record<ChainName, ChainMetadata> = {
monad: {
label: "Monad",
coin: 10143,
evm: true,
},
};

Expand Down
26 changes: 26 additions & 0 deletions packages/offchain-manager/src/offchain-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
import { SubnameDTO } from "../dto/subname.dto";
import {
_addAddressRecord,
_setDefaultEthereumAddress,
_addDataRecord,
_addTextRecord,
_createSubname,
Expand Down Expand Up @@ -185,6 +186,19 @@ export interface OffchainClient {
*/
deleteAddressRecord(subname: string, chain: ChainName): Promise<void>;

/**
* Set a default EVM address for all EVM-compatible chains to a subname.
* This sets the same address for Ethereum, Arbitrum, Optimism, Base, Polygon, BSC, Avalanche, Gnosis, zkSync, Linea, Scroll, Unichain, Berachain, WorldChain, Zora, Celo, and Monad.
* @param subname - Full subname (e.g., 'alice.example.eth')
* @param value - EVM wallet address to set as default for all supported EVM chains
* @throws {ValidationError} When address format is invalid
* @example
* ```typescript
* await client.setDefaultEvmAddress('alice.example.eth', '0x...');
* ```
*/
setDefaultEvmAddress(subname: string, value: string): Promise<void>;

/**
* Add a text record to a subname.
* @param subname - Full subname (e.g., 'alice.example.eth')
Expand Down Expand Up @@ -391,6 +405,18 @@ class HttpOffchainClient implements OffchainClient {
);
}

public async setDefaultEvmAddress(
subname: string,
value: string
): Promise<void> {
await _setDefaultEthereumAddress(
this.HTTP,
this.fetchApiKeyForName(subname),
subname,
value
);
}

public async getSingleSubname(
fullSubname: string
): Promise<SubnameDTO | null> {
Expand Down
30 changes: 30 additions & 0 deletions packages/offchain-manager/src/offchain-client/private-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
subnameResponseToRequest,
} from "./utils";
import { CreateSubnameRequest } from "../dto/create-subname-request.dto";
import { chainMetadata } from "../dto/chains";
import { CreateSubnameRequest_Internal } from "../dto/internal-types";
import { _getSingleSubname } from "./public-actions";
import { UpdateSubnameRequest } from "../dto";
Expand Down Expand Up @@ -205,6 +206,35 @@ export const _deleteDataRecord = async (
});
};

export const _setDefaultEthereumAddress = async (
client: AxiosInstance,
apiKey: string,
fullSubname: string,
value: string
) => {
const subname = await _getSingleSubname(client, fullSubname);

const addresses = subname.addresses || {};

// Derive EVM chains from shared chainMetadata (evm=true)
Object.values(chainMetadata)
.filter((meta) => meta.evm)
.forEach((meta) => {
addresses[meta.coin] = value;
});

const _req = subnameResponseToRequest(subname);

const request: CreateSubnameRequest_Internal = {
..._req,
addresses: mapAddrMapToAddressRecords(addresses),
};

return client.post(`/api/v1/subnames`, request, {
headers: createAuthorizationHeaders(apiKey),
});
};

const createAuthorizationHeaders = (apiKey: string) => {
return {
[AUTH_HEADER]: `${apiKey}`,
Expand Down
47 changes: 47 additions & 0 deletions scripts/format-changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
Format changelog input for proper markdown formatting.
This handles the conversion from single-line GitHub input to properly formatted markdown.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


import sys
import re

def format_changelog(input_text):
"""Format changelog text with proper markdown structure."""

# Convert escaped newlines to actual newlines
text = input_text.replace('\\n', '\n').replace('\\1', '\n')

# Trim whitespace
text = text.strip()

# Fix section headers that appear mid-text - add newlines before them
# Example: "text ### Added" -> "text\n\n### Added"
text = re.sub(r'([^\n]) (### [A-Za-z]+)', r'\1\n\n\2', text)

# Fix section headers - ensure they have proper newlines after the section name
# Example: "### Added - item" -> "### Added\n- item"
text = re.sub(r'^(### [A-Za-z]+) - ', r'\1\n- ', text, flags=re.MULTILINE)

# Fix bullet points that are inline with text
# Example: "text - item" -> "text\n- item"
text = re.sub(r'([^\n]) - ', r'\1\n- ', text)

# Ensure section headers have blank line after them if followed by content
text = re.sub(r'(^### [A-Za-z]+)\n([^-\n])', r'\1\n\n\2', text, flags=re.MULTILINE)

# Clean up multiple newlines (max 2 consecutive)
text = re.sub(r'\n{3,}', '\n\n', text)

return text

if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: format-changelog.py <changelog_input>", file=sys.stderr)
sys.exit(1)

input_text = ' '.join(sys.argv[1:])
formatted = format_changelog(input_text)
print(formatted)