import { base58 } from "../lib/base58"; import { bech32 } from "../lib/bech32"; import { Buffer28, RawCredential } from "../types"; import {Buffer} from "buffer"; /** * @see https://cips.cardano.org/cip/CIP-19 * * The first byte of the binary address, the bits represent following: * - (t t t t n n n n) (t=tag, n=network) * Serialization format * Header type (t t t t . . . .) Payment Part Delegation Part * (0) 0000.... PaymentKeyHash StakeKeyHash * (1) 0001.... ScriptHash StakeKeyHash * (2) 0010.... PaymentKeyHash ScriptHash * (3) 0011.... ScriptHash ScriptHash * (4) 0100.... PaymentKeyHash Pointer * (5) 0101.... ScriptHash Pointer * (6) 0110.... PaymentKeyHash ø * (7) 0111.... ScriptHash ø */ export const KEY_HASH_LENGTH = 28; export const ADDR_LENGTH = KEY_HASH_LENGTH * 2 + 1; export const ADDR_ENTRPRISE_LENGTH = KEY_HASH_LENGTH + 1; export type RawAddress = Buffer; export type RawRewardAccount = Buffer; export type RawAddrKeyHash = Buffer; //28 export type RawVrfKeyHash = Buffer; //32 export class Credential { readonly keyHash?: Buffer28; readonly scriptHash?: Buffer28; // Raw Credential bytes. readonly bytes: Buffer28; constructor(data: Buffer, isScript: boolean) { this.bytes = data; if (isScript) { this.scriptHash = data; } else if (isScript !== undefined) { this.keyHash = data; } } get isScript() { return !!this.scriptHash; } public static fromScriptHash(hash: Buffer): Credential { return new Credential(hash, true); } public static fromKeyHash(hash: Buffer) { return new Credential(hash, false); } public asStakeCredential(): StakeCredential { // @ts-ignore return this; } public static fromCborObject(cred: RawCredential) { switch (cred[0]) { case 0: return Credential.fromKeyHash(cred[1]); case 1: return Credential.fromScriptHash(cred[1]); default: throw new Error(`Invalid Credential type: ${cred[0]}`); } } public static schema(id: string = "credential", label: string = "Credential"): any { return { id: "credential", label: "Credential", type: "string", properties: [ { id: "keyHash", label: "KeyHash", type: "string", validator: (arg: string) => { try { if (arg.length == KEY_HASH_LENGTH * 2) return Credential.fromKeyHash(Buffer.from(arg, "hex")); } catch (error) {} return undefined; }, }, { id: "scriptHash", label: "ScriptHash", type: "string", validator: (arg: string) => { try { if (arg.length == KEY_HASH_LENGTH * 2) return Credential.fromScriptHash(Buffer.from(arg, "hex")); } catch (error) { return undefined; } return undefined; }, }, ], }; } } export interface Address { toBech32(): string; toBytes(): Buffer; equals(addr: Address): boolean; } export class StakeCredential extends Credential { private numbers: number[] = []; constructor(rawBytes: Buffer, isScript: boolean | undefined, __internal_numbers?: number[]) { // Always call super with a boolean for isScript. For pointer credentials (isScript === undefined), // they are not script-based, so we can pass false. super(rawBytes, isScript === undefined ? false : isScript); if (isScript === undefined) { // This is a pointer credential if (__internal_numbers) { this.numbers = __internal_numbers; } else { this.numbers = parsePointerBuffer(rawBytes); } } } public static override fromScriptHash(hash: Buffer): StakeCredential { return new StakeCredential(hash, true); } public static override fromKeyHash(hash: Buffer): StakeCredential { return new StakeCredential(hash, false); } public static fromPointers(slotNo: number, transactionIndex: number, certificateIndex: number) { let numbers = [slotNo, transactionIndex, certificateIndex]; const bytes = convertToVariableLengthUintArray(numbers); return new StakeCredential(bytes, undefined, numbers); } public static override fromCborObject(cred: RawCredential): StakeCredential { return Credential.fromCborObject(cred).asStakeCredential(); } get slotNo(): number | undefined { return this.numbers[0]; } get transactionIndex(): number | undefined { return this.numbers[1]; } get certificateIndex(): number | undefined { return this.numbers[2]; } public isPointerCredential() { return this.numbers.length > 0; } } export class DrepCredential extends Credential implements Address{ constructor(hash_bytes: Buffer28, isScript: boolean) { super(hash_bytes,isScript) } toBytes(): Buffer { return this.bytesCip129() } equals(addr: Address): boolean { return (addr instanceof DrepCredential && (addr.toBytes().compare(this.toBytes()) == 0)) } toBech32= this.toBech32Cip129 toBech32Cip105(): string{ if(this.isScript){ return bech32.encode('drep_script', this.bytes) }else{ return bech32.encode('drep', this.bytes) } } toBech32Cip129():string{ return bech32.encode('drep', this.bytesCip129()) } static fromBytes(rawData:Buffer,_isScrit=false):DrepCredential{ let isScript:boolean,dataHeader if(rawData.length == KEY_HASH_LENGTH){ isScript = false }else if(rawData.length !=(KEY_HASH_LENGTH +1)){ throw new Error(`Invalid byte size when decoding drep credential:${rawData.toString('hex')}`); }else{ dataHeader = rawData[0]! if(dataHeader == 0x22){ isScript = false }else if (dataHeader == 0x23){ isScript = true }else{ throw new Error("Drep credential contains invalid header 0x"+ dataHeader.toString(16)+ ": "+rawData.toString('hex')) } rawData = rawData.subarray(1) } return new DrepCredential(rawData,_isScrit=== undefined? isScript:_isScrit) } static fromBech32(data:string):DrepCredential{ let decoded= bech32.decode(data) if(decoded.prefix == 'drep_script'){ return DrepCredential.fromBytes(decoded.data,true) }if(decoded.prefix == 'drep'){ return DrepCredential.fromBytes(decoded.data) }else{ throw new Error ("Not a Drep credential : "+data) } } bytesCip129(): Buffer { let rawBytes = new Uint8Array(KEY_HASH_LENGTH + 1); if(this.keyHash){ rawBytes[0]=0x22 }else{ rawBytes[0]=0x23 } rawBytes.set(this.bytes,1) return Buffer.from(rawBytes) } } export class ShelleyAddress implements Address { readonly networkId: number; readonly stakePart?: StakeCredential; readonly paymentPart: Credential; private raw?: Buffer; constructor(networkId: number, paymentPart: Credential, stakePart?: StakeCredential) { this.networkId = networkId; this.paymentPart = paymentPart; this.stakePart = stakePart; } public static fromBytes(rawBytes: Buffer) { let header = rawBytes[0]!; let network = header & 0x0f; let isPaymentScript = (header & 0b00010000) != 0; let stakePartInfo = (header & 0b01110000) >> 5; const paymentCred = new Credential(rawBytes.subarray(1, 29), isPaymentScript); let stakeCred: StakeCredential | undefined; if (stakePartInfo == 0b00 || stakePartInfo == 0b01) { // keyHash or scriptHash if (rawBytes.length !== ADDR_LENGTH) { throw Error(`ShelleyAddress.fromBytes: Invalid byte array length for key/script hash stake part: ${rawBytes.length}. Expected ${ADDR_LENGTH}`); } stakeCred = (stakePartInfo == 0b00) ? StakeCredential.fromKeyHash(rawBytes.subarray(29, ADDR_LENGTH)) : StakeCredential.fromScriptHash(rawBytes.subarray(29, ADDR_LENGTH)); } else if (stakePartInfo == 0b10) { // pointer address if (rawBytes.length < (1 + KEY_HASH_LENGTH + 3)) { // 1 (header) + 28 (payment) + 3 (min pointer) = 32 throw Error(`ShelleyAddress.fromBytes: Invalid byte array length for pointer stake part: ${rawBytes.length}. Minimum expected 32.`); } stakeCred = new StakeCredential(rawBytes.subarray(29, rawBytes.length), undefined); } else if (stakePartInfo == 0b11) { // enterprise address ie no stake part. if (rawBytes.length !== ADDR_ENTRPRISE_LENGTH) { throw Error(`ShelleyAddress.fromBytes: Invalid byte array length for enterprise address: ${rawBytes.length}. Expected ${ADDR_ENTRPRISE_LENGTH}`); } } else { throw Error(`ShelleyAddress.fromBytes: Unknown stake part info: ${stakePartInfo}`); } const addr = new ShelleyAddress(network, paymentCred, stakeCred); addr.raw = rawBytes; return addr; } isMainnet() { return this.networkId === 1; } isTestnet() { return this.networkId !== 1; } toBytes(): Buffer { if (!this.raw) { let byteLen = ADDR_ENTRPRISE_LENGTH; let rawBytes: Uint8Array; let header = this.networkId & 0x0f; if (this.paymentPart.isScript) { header |= 0b00010000; } if (this.stakePart) { const stakeBytes = this.stakePart.bytes; byteLen = 1 + KEY_HASH_LENGTH + stakeBytes.length; rawBytes = new Uint8Array(byteLen); rawBytes.set(stakeBytes, KEY_HASH_LENGTH + 1); if (this.stakePart?.isPointerCredential()) { header |= 0b01000000; } else if (this.stakePart?.isScript) { header |= 0b00100000; } } else { header |= 0b01100000; rawBytes = new Uint8Array(byteLen); } rawBytes[0] = header; rawBytes.set(this.paymentPart.bytes, 1); this.raw = Buffer.from(rawBytes); } return this.raw; } toBech32(): string { const prefix = this.isTestnet() ? "addr_test" : "addr"; return bech32.encode(prefix, Buffer.from(this.toBytes())); } public static fromBech32(addrStr: string): ShelleyAddress { const decoded = bech32.decode(addrStr); return ShelleyAddress.fromBytes(decoded.data); } public static fromAny(addr: string | Buffer): ShelleyAddress { if (Buffer.isBuffer(addr)) { return this.fromBytes(addr); } if (typeof addr == "string") { try { const hexPattern = /^(?:0x)?[a-fA-F0-9]{2,}(?:[a-fA-F0-9]{2})*$/; if (hexPattern.test(addr)) { return this.fromBytes(Buffer.from(addr, "hex")); } else { const bech32Words = Buffer.from(bech32.decode(addr).data); return this.fromBytes(bech32Words); } } catch (error: any) { throw new Error(`Error de-serializing address: ${error || error.message}`); } } else { throw new Error("ShelleyAddress.fromAny: Usupported type " + typeof addr); } } static compare(addr1: ShelleyAddress, addr2: ShelleyAddress) { return Buffer.compare(addr1.toBytes(), addr2.toBytes()); } equals(addr: ShelleyAddress): boolean { return ShelleyAddress.compare(this, addr) == 0; } toJSON() { return this.toBech32(); } } export class StakeAddress implements Address { networkId: number; credential: StakeCredential; constructor(networkId: number, stakeCredential: StakeCredential) { this.networkId = networkId; this.credential = stakeCredential; } isMainnet() { return this.networkId === 1; } isTestnet() { return this.networkId !== 1; } public static fromBytes(rawBytes: Buffer): StakeAddress { if (rawBytes.length > ADDR_ENTRPRISE_LENGTH || rawBytes.length < 4) { throw Error("StakeAddress.fromBytes: Invalid byte array length:" + rawBytes.length); } let credentialBytes = rawBytes.subarray(1); let credential; let header = rawBytes[0]!; let network = header & 0x0f; let addressType = (header & 0b11110000) >> 4; if (addressType == 0b1110) { // keyHash credential = StakeCredential.fromKeyHash(credentialBytes); } else if (addressType == 0b1111) { credential = StakeCredential.fromScriptHash(credentialBytes); } else { throw new Error( "StakeAddress.fromBytes: Pointer addresses cannot be converted to a standalone StakeAddress/RewardAccount" ); } return new StakeAddress(network, credential); } toBytes(): Buffer { const stakeBytes = this.credential.bytes; let byteLen = 1 + stakeBytes.length; const rawBytes = new Uint8Array(byteLen); rawBytes.set(stakeBytes, 1); if (this.credential.isPointerCredential()) { throw new Error("StakeAddress.toBytes: Pointer credentials are not supported for standalone StakeAddress/RewardAccount"); } const addressType = this.credential.isScript ? 0b1111 : 0b1110; const header = (addressType << 4) | (this.networkId & 0x0f); rawBytes[0] = header; return Buffer.from(rawBytes); } toBech32(): string { const prefix = this.isTestnet() ? "stake_test" : "stake"; return bech32.encode(prefix, Buffer.from(this.toBytes())); } public static fromBech32(addrStr: string): StakeAddress { const decoded = bech32.decode(addrStr); return StakeAddress.fromBytes(decoded.data); } toJSON() { return this.toBech32(); } static compare(addr1: StakeAddress, addr2: StakeAddress) { return Buffer.compare(addr1.toBytes(), addr2.toBytes()); } greaterThan(addr: StakeAddress): boolean { return StakeAddress.compare(this, addr) > 0; } lessThan(addr: StakeAddress): boolean { return StakeAddress.compare(this, addr) < 0; } equals(addr: StakeAddress): boolean { return StakeAddress.compare(this, addr) == 0; } public static schema(id: string = "stakeAddress", label: string = "StakeAddress"): any { return { id, label, type: "string", properties: [ { id: "networkId", label: "NetworkId", type: "number", }, Credential.schema(), ], validator: (arg: string) => { try { return StakeAddress.fromBech32(arg); } catch (error: any) { try { StakeAddress.fromBytes(Buffer.from(arg, "hex")); } catch (err: any) {} } return undefined; }, }; } } export class ByronAddress implements Address { addressBytes: Buffer; constructor(addressBytes: Buffer) { this.addressBytes = addressBytes; } public static fromBytes(rawBytes: Buffer) { let header = rawBytes[0]!; let firstNibble = (header & 0b111000) >> 4; if (firstNibble != 0b1000) { throw new Error( "ByronAddress.fromBytes: First 4 bit of byteArray is " + firstNibble.toString(1) + "! Which is not possible" ); } return new ByronAddress(rawBytes); } toBytes() { return this.addressBytes; } toBech32(): string { return base58.encode(this.addressBytes); } public static compare(addr1: ByronAddress, addr2: ByronAddress) { return Buffer.compare(addr1.toBytes(), addr2.toBytes()); } greaterThan(addr: ByronAddress): boolean { return ByronAddress.compare(this, addr) > 0; } lessThan(addr: ByronAddress): boolean { return ByronAddress.compare(this, addr) < 0; } equals(addr: ByronAddress): boolean { return ByronAddress.compare(this, addr) == 0; } } export function parseAddressBytes(rawBytes: Buffer) { let header = rawBytes[0]!; let firstNibble = (header & 0b11110000) >> 4; if (firstNibble == 0b1000) { return new ByronAddress(rawBytes); } else { return ShelleyAddress.fromBytes(rawBytes); } } function parsePointerBuffer(buffer: Buffer): number[] { let index = 0; const result: number[] = []; while (index < buffer.length) { let value = 0; let shift = 0; // Process a VARIABLE-LENGTH-UINT, stopping when the MSB is 0 while (buffer[index] & 0x80) { // Check if MSB is 1 value |= (buffer[index] & 0x7f) << shift; // Mask out MSB and shift the rest shift += 7; index++; } // Handle last byte, which has MSB = 0 value |= (buffer[index] & 0x7f) << shift; index++; result.push(value); } return result; } export type RewardAccount = StakeAddress; function convertToVariableLengthUintArray(numbers: number[]): Buffer { let result: number[] = []; numbers.forEach((num) => { let bytes: number[] = []; // Handle encoding the number do { let byte = num & 0x7f; // Take the lowest 7 bits num >>= 7; // Right shift by 7 bits if (num > 0) { byte |= 0x80; // Set the continuation bit (more bytes follow) } bytes.push(byte); } while (num > 0); // Append the bytes to the result array result.push(...bytes); }); return Buffer.from(result); }