diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 537036d..de07ea1 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -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 @@ -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" diff --git a/changelog/offchain-manager-changelog.md b/changelog/offchain-manager-changelog.md index d83a2a8..d294a33 100644 --- a/changelog/offchain-manager-changelog.md +++ b/changelog/offchain-manager-changelog.md @@ -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 diff --git a/packages/offchain-manager/README.md b/packages/offchain-manager/README.md index b51130a..923258a 100644 --- a/packages/offchain-manager/README.md +++ b/packages/offchain-manager/README.md @@ -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 diff --git a/packages/offchain-manager/examples/basic-usage.ts b/packages/offchain-manager/examples/basic-usage.ts index 75e540e..7b1ced8 100644 --- a/packages/offchain-manager/examples/basic-usage.ts +++ b/packages/offchain-manager/examples/basic-usage.ts @@ -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', diff --git a/packages/offchain-manager/src/dto/chains.ts b/packages/offchain-manager/src/dto/chains.ts index c5be92b..76f3095 100644 --- a/packages/offchain-manager/src/dto/chains.ts +++ b/packages/offchain-manager/src/dto/chains.ts @@ -79,6 +79,8 @@ 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; } @@ -86,42 +88,52 @@ export const chainMetadata: Record = { 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", @@ -146,10 +158,12 @@ export const chainMetadata: Record = { linea: { label: "Linea", coin: 59144, + evm: true, }, scroll: { label: "Scroll", coin: 534352, + evm: true, }, sui: { label: "Sui", @@ -158,22 +172,27 @@ export const chainMetadata: Record = { 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", @@ -186,6 +205,7 @@ export const chainMetadata: Record = { monad: { label: "Monad", coin: 10143, + evm: true, }, }; diff --git a/packages/offchain-manager/src/offchain-client/index.ts b/packages/offchain-manager/src/offchain-client/index.ts index 1965cca..94e4ad3 100644 --- a/packages/offchain-manager/src/offchain-client/index.ts +++ b/packages/offchain-manager/src/offchain-client/index.ts @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; import { SubnameDTO } from "../dto/subname.dto"; import { _addAddressRecord, + _setDefaultEthereumAddress, _addDataRecord, _addTextRecord, _createSubname, @@ -185,6 +186,19 @@ export interface OffchainClient { */ deleteAddressRecord(subname: string, chain: ChainName): Promise; + /** + * 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; + /** * Add a text record to a subname. * @param subname - Full subname (e.g., 'alice.example.eth') @@ -391,6 +405,18 @@ class HttpOffchainClient implements OffchainClient { ); } + public async setDefaultEvmAddress( + subname: string, + value: string + ): Promise { + await _setDefaultEthereumAddress( + this.HTTP, + this.fetchApiKeyForName(subname), + subname, + value + ); + } + public async getSingleSubname( fullSubname: string ): Promise { diff --git a/packages/offchain-manager/src/offchain-client/private-actions.ts b/packages/offchain-manager/src/offchain-client/private-actions.ts index 04cfbf0..fba2832 100644 --- a/packages/offchain-manager/src/offchain-client/private-actions.ts +++ b/packages/offchain-manager/src/offchain-client/private-actions.ts @@ -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"; @@ -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}`, diff --git a/scripts/format-changelog.py b/scripts/format-changelog.py new file mode 100755 index 0000000..d9ca963 --- /dev/null +++ b/scripts/format-changelog.py @@ -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. +""" + +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 ", file=sys.stderr) + sys.exit(1) + + input_text = ' '.join(sys.argv[1:]) + formatted = format_changelog(input_text) + print(formatted) +