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
1 change: 1 addition & 0 deletions packages/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from "./query";
export * from "./solana";
export * from "./sui";
export * from "./ton";
export * from "./utils";
export * from "./zetachain";
2 changes: 2 additions & 0 deletions packages/commands/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { queryCommand } from "./query";
import { solanaCommand } from "./solana";
import { suiCommand } from "./sui";
import { tonCommand } from "./ton";
import { utilsCommand } from "./utils";
import { zetachainCommand } from "./zetachain";

export const toolkitCommand = new Command("toolkit")
Expand All @@ -24,6 +25,7 @@ toolkitCommand.addCommand(queryCommand);
toolkitCommand.addCommand(solanaCommand);
toolkitCommand.addCommand(suiCommand);
toolkitCommand.addCommand(tonCommand);
toolkitCommand.addCommand(utilsCommand);
toolkitCommand.addCommand(zetachainCommand);

showRequiredOptions(toolkitCommand);
Expand Down
46 changes: 46 additions & 0 deletions packages/commands/src/utils/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Command } from "commander";
import { getBorderCharacters, table } from "table";

import { convertAddressAll } from "../../../../utils/address";

export const addressCommand = new Command("address")
.summary(
"Show all address formats (bytes, hex, bech32 acc/valoper/valcons) for any input"
)
.argument(
"<address>",
"Address in hex (0x... or without 0x), bech32, or [..] bytes"
)
.option("--json", "Output results as JSON")
.action((address: string, options: { json?: boolean }) => {
try {
const result = convertAddressAll(address);
if (options.json) {
console.log(
JSON.stringify(
{
bech32Acc: result.bech32Acc,
bech32Valcons: result.bech32Valcons,
bech32Valoper: result.bech32Valoper,
checksummedAddress: result.checksummedAddress,
hexUppercase: result.hexUppercase,
},
null,
2
)
);
return;
}
const rows = [
["Format", "Address"],
["Address (hex)", result.checksummedAddress],
["Bech32 Acc", result.bech32Acc],
["Bech32 Val", result.bech32Valoper],
["Bech32 Con", result.bech32Valcons],
];
console.log(table(rows, { border: getBorderCharacters("norc") }));
} catch (error) {
console.error("Failed to convert address:", error);
process.exit(1);
}
});
8 changes: 8 additions & 0 deletions packages/commands/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Command } from "commander";

import { addressCommand } from "./address";

export const utilsCommand = new Command("utils")
.summary("Utility commands")
.addCommand(addressCommand)
.helpCommand(false);
13 changes: 2 additions & 11 deletions packages/tasks/src/account.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { input } from "@inquirer/prompts";
import { Keypair } from "@solana/web3.js";
import { bech32 } from "bech32";
import { validateMnemonic } from "bip39";
import bs58 from "bs58";
import * as envfile from "envfile";
Expand All @@ -12,17 +11,9 @@ import * as path from "path";

import { numberArraySchema } from "../../../types/shared.schema";
import { handleError, parseJson } from "../../../utils";
import { hexToBech32 } from "../../../utils/address";
import { generateBitcoinAddress } from "../../../utils/generateBitcoinAddress";

export const hexToBech32Address = (
hexAddress: string,
prefix: string
): string => {
const data = Buffer.from(hexAddress.substr(2), "hex");
const words = bech32.toWords(data);
return bech32.encode(prefix, words);
};

export const getWalletFromRecoveryInput = async (
ethers: HardhatRuntimeEnvironment["ethers"]
) => {
Expand Down Expand Up @@ -153,7 +144,7 @@ export const main = async (
console.log(`
😃 EVM address: ${address}
😃 Bitcoin address: ${generateBitcoinAddress(pk, "testnet")}
😃 Bech32 address: ${hexToBech32Address(address, "zeta")}`);
😃 Bech32 address: ${hexToBech32(address)}`);

if (solanaWallet) {
console.log(`
Expand Down
142 changes: 142 additions & 0 deletions utils/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { bech32 } from "bech32";
import { ethers } from "ethers";

export const ZETA_HRP = "zeta";
export const ZETA_VALOPER_HRP = "zetavaloper";
export const ZETA_VALCONS_HRP = "zetavalcons";

export const isHexAddress = (value: string): boolean => {
try {
const candidate = value.startsWith("0x") ? value : `0x${value}`;
return ethers.isAddress(candidate);
} catch {
return false;
}
};

export const isBech32Address = (value: string): boolean => {
try {
const decoded = bech32.decode(value);
return (
!!decoded && Array.isArray(decoded.words) && decoded.words.length > 0
);
} catch {
return false;
}
};

export const hexToBech32 = (
hexAddr: string,
hrp: string = ZETA_HRP
): string => {
const checksumAddress = ethers.getAddress(hexAddr);
const addressBytes = ethers.getBytes(checksumAddress);
const words = bech32.toWords(Buffer.from(addressBytes));
return bech32.encode(hrp, words);
};

export const bech32ToHex = (bech32Addr: string): string => {
const decoded = bech32.decode(bech32Addr);
const bytes = Buffer.from(bech32.fromWords(decoded.words));
if (bytes.length !== 20) {
throw new Error(
`Invalid address bytes length ${bytes.length}, expected 20`
);
}
const hex = `0x${bytes.toString("hex")}`;
return ethers.getAddress(hex);
};

/**
* Try to parse a string that contains a decimal byte array in square brackets
* e.g. "[73 85 163 ...]" or "Address: [73, 85, 163, ...]".
* Returns an array of numbers if successful, otherwise null.
*/
const tryParseByteArrayString = (value: string): number[] | null => {
const start = value.indexOf("[");
const end = value.lastIndexOf("]");
if (start === -1 || end === -1 || end <= start) return null;
const inner = value.slice(start + 1, end);
const parts = inner
.split(/[,\s]+/)
.map((p) => p.trim())
.filter((p) => p.length > 0);
if (parts.length === 0) return null;
const numbers: number[] = [];
for (const part of parts) {
const n = Number(part);
if (!Number.isInteger(n) || n < 0 || n > 255) return null;
numbers.push(n);
}
if (numbers.length !== 20) return null;
return numbers;
};

/**
* Normalize any supported address input into a 20-byte array.
* Supports: 0x hex, hex without 0x, bech32 (any zeta* HRP), and decimal byte array strings.
*/
const parseAddressInput = (input: string): Uint8Array => {
const value = input.trim();

if (isHexAddress(value)) {
const checksum = ethers.getAddress(
value.startsWith("0x") ? value : `0x${value}`
);
return Uint8Array.from(ethers.getBytes(checksum));
}

if (isBech32Address(value)) {
const hex = bech32ToHex(value);
return Uint8Array.from(ethers.getBytes(hex));
}

const bytesArray = tryParseByteArrayString(value);
if (bytesArray) {
return Uint8Array.from(bytesArray);
}

throw new Error(
"Unsupported address format. Provide hex (0x... or without 0x), bech32, or a [..] byte array."
);
};

export interface UnifiedAddressFormats {
bech32Acc: string;
bech32Valcons: string;
bech32Valoper: string;
bytes: number[];
// EIP-55 checksummed address with 0x prefix
checksummedAddress: string;
// uppercase hex without 0x prefix
hexUppercase: string;
}

/**
* Convert an input address in any supported form into all common representations.
*/
export const convertAddressAll = (input: string): UnifiedAddressFormats => {
const bytes = parseAddressInput(input);
if (bytes.length !== 20) {
throw new Error(
`Invalid address length ${bytes.length}, expected 20 bytes`
);
}

const hexLower = Buffer.from(bytes).toString("hex");
const checksummedAddress = ethers.getAddress(`0x${hexLower}`);

const hexUppercase = hexLower.toUpperCase();
const bech32Acc = hexToBech32(checksummedAddress, ZETA_HRP);
const bech32Valoper = hexToBech32(checksummedAddress, ZETA_VALOPER_HRP);
const bech32Valcons = hexToBech32(checksummedAddress, ZETA_VALCONS_HRP);

return {
bech32Acc,
bech32Valcons,
bech32Valoper,
bytes: Array.from(bytes),
checksummedAddress,
hexUppercase,
};
};
1 change: 1 addition & 0 deletions utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "../src/chains/bitcoin/inscription/encode";
export * from "./address";
export * from "./api";
export * from "./bitsize";
export * from "./compareBigIntAndNumber";
Expand Down