From d3c1a3e6f1d1e65f9069049ddee65bc187f06f7d Mon Sep 17 00:00:00 2001 From: Samrendra Date: Fri, 7 Feb 2025 13:03:48 +0530 Subject: [PATCH 1/3] feat(provider): implement Brother ID name service extension --- src/provider/extensions/brotherId.ts | 228 +++++++++++++++++++++++++++ src/provider/extensions/default.ts | 3 +- 2 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/provider/extensions/brotherId.ts diff --git a/src/provider/extensions/brotherId.ts b/src/provider/extensions/brotherId.ts new file mode 100644 index 000000000..4f66260d7 --- /dev/null +++ b/src/provider/extensions/brotherId.ts @@ -0,0 +1,228 @@ +import { BigNumberish } from '../../types'; +import { CallData } from '../../utils/calldata'; +import { decodeShortString, encodeShortString } from '../../utils/shortString'; +import type { ProviderInterface } from '..'; +import { StarknetChainId } from '../../global/constants'; + +/** + * Validates if a domain is a valid .brother domain + * @param domain - Domain name to validate + * @returns true if the domain is valid + */ +export function isBrotherDomain(domain: string): boolean { + return domain.endsWith('.brother'); +} + +/** + * Get the Brother ID contract address for the specified network + * @param chainId - The Starknet chain ID + * @returns The Brother ID contract address for the network + */ +export function getBrotherIdContract(chainId: StarknetChainId): string { + switch (chainId) { + case StarknetChainId.SN_MAIN: + return '0x0212f1c57700f5a3913dd11efba540196aad4cf67772f7090c62709dd804fa74'; + default: + return '0x0212f1c57700f5a3913dd11efba540196aad4cf67772f7090c62709dd804fa74'; // Default to mainnet address + } +} + +/** + * Interface representing a Brother domain profile + * @property name - The domain name without .brother suffix + * @property resolver - The address that resolves to this domain + * @property tokenId - The unique identifier of the domain NFT + * @property expiryDate - Unix timestamp when the domain expires + * @property lastTransferTime - Unix timestamp of the last transfer + */ +export interface BrotherProfile { + name: string; + resolver: string; + tokenId: string; + expiryDate: number; + lastTransferTime: number; +} + +/** + * Class providing methods to interact with Brother Identity contracts + */ +export class BrotherId { + /** + * Gets the primary Brother domain name for an address + * @param address - The address to get the domain for + * @param BrotherIdContract - Optional contract address + * @returns The domain name with .brother suffix + */ + async getBrotherName(address: BigNumberish, BrotherIdContract?: string) { + return BrotherId.getBrotherName( + // After Mixin, this is ProviderInterface + (this) as ProviderInterface, + address, + BrotherIdContract + ); + } + + /** + * Gets the address associated with a Brother domain name + * @param name - The domain name (with or without .brother suffix) + * @param BrotherIdContract - Optional contract address + * @returns The resolver address for the domain + */ + public async getAddressFromBrotherName( + name: string, + BrotherIdContract?: string + ): Promise { + return BrotherId.getAddressFromBrotherName( + // After Mixin, this is ProviderInterface + (this) as ProviderInterface, + name, + BrotherIdContract + ); + } + + /** + * Gets the complete profile information for a Brother domain + * @param address - The address to get the profile for + * @param BrotherIdContract - Optional contract address + * @returns The complete Brother profile information + */ + async getBrotherProfile(address: BigNumberish, BrotherIdContract?: string) { + return BrotherId.getBrotherProfile( + // After Mixin, this is ProviderInterface + (this) as ProviderInterface, + address, + BrotherIdContract + ); + } + + /** + * Static implementation of getBrotherName + * @param provider - The provider interface + * @param address - The address to get the domain for + * @param BrotherIdContract - Optional contract address + * @returns The domain name with .brother suffix + */ + static async getBrotherName( + provider: ProviderInterface, + address: BigNumberish, + BrotherIdContract?: string + ): Promise { + const chainId = await provider.getChainId(); + const contract = BrotherIdContract ?? getBrotherIdContract(chainId); + + try { + const primaryDomain = await provider.callContract({ + contractAddress: contract, + entrypoint: 'getPrimary', + calldata: CallData.compile({ + user: address, + }), + }); + + if (!primaryDomain[0] || primaryDomain[0] === '0x0') { + throw Error('Brother name not found'); + } + + const domain = decodeShortString(primaryDomain[0]); + return `${domain}.brother`; + } catch (e) { + if (e instanceof Error && e.message === 'Brother name not found') { + throw e; + } + throw Error('Could not get brother name'); + } + } + + /** + * Static implementation of getAddressFromBrotherName + * @param provider - The provider interface + * @param name - The domain name + * @param BrotherIdContract - Optional contract address + * @returns The resolver address + */ + static async getAddressFromBrotherName( + provider: ProviderInterface, + name: string, + BrotherIdContract?: string + ): Promise { + const brotherName = name.endsWith('.brother') ? name : `${name}.brother`; + + if (!isBrotherDomain(brotherName)) { + throw new Error('Invalid domain, must be a valid .brother domain'); + } + + const chainId = await provider.getChainId(); + const contract = BrotherIdContract ?? getBrotherIdContract(chainId); + + try { + const domainDetails = await provider.callContract({ + contractAddress: contract, + entrypoint: 'get_details_by_domain', + calldata: CallData.compile({ + domain: encodeShortString(brotherName.replace('.brother', '')), + }), + }); + + if (!domainDetails[0] || domainDetails[1] === '0x0') { + throw Error('Could not get address from brother name'); + } + + return domainDetails[1]; // resolver address + } catch { + throw Error('Could not get address from brother name'); + } + } + + /** + * Static implementation of getBrotherProfile + * @param provider - The provider interface + * @param address - The address to get the profile for + * @param BrotherIdContract - Optional contract address + * @returns The complete Brother profile + */ + static async getBrotherProfile( + provider: ProviderInterface, + address: BigNumberish, + BrotherIdContract?: string + ): Promise { + const chainId = await provider.getChainId(); + const contract = BrotherIdContract ?? getBrotherIdContract(chainId); + + try { + const primaryDomain = await provider.callContract({ + contractAddress: contract, + entrypoint: 'getPrimary', + calldata: CallData.compile({ + user: address, + }), + }); + + if (!primaryDomain[0] || primaryDomain[0] === '0x0') { + throw Error('Brother profile not found'); + } + + const domain = decodeShortString(primaryDomain[0]); + + const domainDetails = await provider.callContract({ + contractAddress: contract, + entrypoint: 'get_details_by_domain', + calldata: CallData.compile({ + domain: encodeShortString(domain), + }), + }); + + return { + name: domain, + resolver: domainDetails[1], + tokenId: domainDetails[2], + expiryDate: parseInt(domainDetails[3], 16), + lastTransferTime: parseInt(domainDetails[4], 16), + }; + } catch (e) { + if (e instanceof Error && e.message === 'Brother profile not found') { + throw e; + } + throw Error('Could not get brother profile'); + } + } +} diff --git a/src/provider/extensions/default.ts b/src/provider/extensions/default.ts index 2b813ab80..83dad0d32 100644 --- a/src/provider/extensions/default.ts +++ b/src/provider/extensions/default.ts @@ -3,5 +3,6 @@ import { Mixin } from 'ts-mixer'; import { RpcProvider as BaseRpcProvider } from '../rpc'; import { StarknetId } from './starknetId'; +import { BrotherId } from './brotherId'; -export class RpcProvider extends Mixin(BaseRpcProvider, StarknetId) {} +export class RpcProvider extends Mixin(BaseRpcProvider, StarknetId, BrotherId) {} From c8dac96792e8b67b884da6a443f94c3f7dfc9799 Mon Sep 17 00:00:00 2001 From: Samrendra Date: Thu, 13 Mar 2025 20:40:38 +0530 Subject: [PATCH 2/3] feat(provider): add Brother Identity extension and utilities --- src/provider/extensions/brotherId.ts | 51 +++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/provider/extensions/brotherId.ts b/src/provider/extensions/brotherId.ts index 4f66260d7..18bcdffec 100644 --- a/src/provider/extensions/brotherId.ts +++ b/src/provider/extensions/brotherId.ts @@ -1,8 +1,9 @@ import { BigNumberish } from '../../types'; import { CallData } from '../../utils/calldata'; -import { decodeShortString, encodeShortString } from '../../utils/shortString'; +import { decodeShortString } from '../../utils/shortString'; import type { ProviderInterface } from '..'; import { StarknetChainId } from '../../global/constants'; +import { useEncoded, useDecoded } from '../../utils/starknetId'; /** * Validates if a domain is a valid .brother domain @@ -13,6 +14,43 @@ export function isBrotherDomain(domain: string): boolean { return domain.endsWith('.brother'); } +/** + * Encodes a Brother domain name into a bigint value. + * This uses the same encoding logic as Starknet ID. + * @param domain - The domain name without .brother suffix + * @returns encoded bigint value + * @example + * ```typescript + * const encoded = encodeBrotherDomain("myname.brother"); + * // Returns a bigint value + * ``` + */ +export function encodeBrotherDomain(domain: string): bigint { + const brotherName = domain.endsWith('.brother') ? domain.replace('.brother', '') : domain; + return useEncoded(brotherName); +} + +/** + * Decodes a bigint value into a Brother domain name. + * This uses the same decoding logic as Starknet ID but returns a .brother domain. + * @param encoded - The encoded bigint value + * @returns The decoded domain name with .brother suffix + * @example + * ```typescript + * const domain = decodeBrotherDomain(1234567890n); + * // Returns "example.brother" + * ``` + */ +export function decodeBrotherDomain(encoded: bigint): string { + const decoded = useDecoded([encoded]); + // Replace .stark with .brother + if (decoded.endsWith('.stark')) { + return decoded.replace('.stark', '.brother'); + } + // If no suffix, add .brother + return decoded ? `${decoded}.brother` : decoded; +} + /** * Get the Brother ID contract address for the specified network * @param chainId - The Starknet chain ID @@ -44,7 +82,12 @@ export interface BrotherProfile { } /** - * Class providing methods to interact with Brother Identity contracts + * Class providing methods to interact with Brother Identity contracts. + * + * This implementation uses the same domain encoding and decoding logic as StarknetId, + * allowing for consistent handling of domain names between the two systems. + * The encoding/decoding functions (encodeBrotherDomain/decodeBrotherDomain) are direct + * adaptations of StarknetId's useEncoded/useDecoded functions to work with .brother domains. */ export class BrotherId { /** @@ -159,7 +202,7 @@ export class BrotherId { contractAddress: contract, entrypoint: 'get_details_by_domain', calldata: CallData.compile({ - domain: encodeShortString(brotherName.replace('.brother', '')), + domain: encodeBrotherDomain(brotherName), }), }); @@ -207,7 +250,7 @@ export class BrotherId { contractAddress: contract, entrypoint: 'get_details_by_domain', calldata: CallData.compile({ - domain: encodeShortString(domain), + domain: encodeBrotherDomain(domain), }), }); From 4fb65f2b6554a0f5832838a2ce8a842f81190ac2 Mon Sep 17 00:00:00 2001 From: Samrendra Date: Thu, 13 Mar 2025 20:55:37 +0530 Subject: [PATCH 3/3] fix(provider): correct domain decoding in BrotherId extension --- src/provider/extensions/brotherId.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/provider/extensions/brotherId.ts b/src/provider/extensions/brotherId.ts index 18bcdffec..2c77fa022 100644 --- a/src/provider/extensions/brotherId.ts +++ b/src/provider/extensions/brotherId.ts @@ -1,6 +1,5 @@ import { BigNumberish } from '../../types'; import { CallData } from '../../utils/calldata'; -import { decodeShortString } from '../../utils/shortString'; import type { ProviderInterface } from '..'; import { StarknetChainId } from '../../global/constants'; import { useEncoded, useDecoded } from '../../utils/starknetId'; @@ -166,8 +165,8 @@ export class BrotherId { throw Error('Brother name not found'); } - const domain = decodeShortString(primaryDomain[0]); - return `${domain}.brother`; + const encodedDomain = BigInt(primaryDomain[0]); + return decodeBrotherDomain(encodedDomain); } catch (e) { if (e instanceof Error && e.message === 'Brother name not found') { throw e; @@ -244,7 +243,9 @@ export class BrotherId { throw Error('Brother profile not found'); } - const domain = decodeShortString(primaryDomain[0]); + const encodedDomain = BigInt(primaryDomain[0]); + const decodedDomain = decodeBrotherDomain(encodedDomain); + const domain = decodedDomain.replace('.brother', ''); const domainDetails = await provider.callContract({ contractAddress: contract,