Skip to content

Commit 0bea966

Browse files
feat(provider): Add Brother ID domain resolution support (#1313)
* feat(provider): implement Brother ID name service extension * feat(provider): add Brother Identity extension and utilities * fix(provider): correct domain decoding in BrotherId extension --------- Co-authored-by: Toni Tabak <tabaktoni@gmail.com>
1 parent d0c0a71 commit 0bea966

File tree

2 files changed

+274
-1
lines changed

2 files changed

+274
-1
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { BigNumberish } from '../../types';
2+
import { CallData } from '../../utils/calldata';
3+
import type { ProviderInterface } from '..';
4+
import { StarknetChainId } from '../../global/constants';
5+
import { useEncoded, useDecoded } from '../../utils/starknetId';
6+
7+
/**
8+
* Validates if a domain is a valid .brother domain
9+
* @param domain - Domain name to validate
10+
* @returns true if the domain is valid
11+
*/
12+
export function isBrotherDomain(domain: string): boolean {
13+
return domain.endsWith('.brother');
14+
}
15+
16+
/**
17+
* Encodes a Brother domain name into a bigint value.
18+
* This uses the same encoding logic as Starknet ID.
19+
* @param domain - The domain name without .brother suffix
20+
* @returns encoded bigint value
21+
* @example
22+
* ```typescript
23+
* const encoded = encodeBrotherDomain("myname.brother");
24+
* // Returns a bigint value
25+
* ```
26+
*/
27+
export function encodeBrotherDomain(domain: string): bigint {
28+
const brotherName = domain.endsWith('.brother') ? domain.replace('.brother', '') : domain;
29+
return useEncoded(brotherName);
30+
}
31+
32+
/**
33+
* Decodes a bigint value into a Brother domain name.
34+
* This uses the same decoding logic as Starknet ID but returns a .brother domain.
35+
* @param encoded - The encoded bigint value
36+
* @returns The decoded domain name with .brother suffix
37+
* @example
38+
* ```typescript
39+
* const domain = decodeBrotherDomain(1234567890n);
40+
* // Returns "example.brother"
41+
* ```
42+
*/
43+
export function decodeBrotherDomain(encoded: bigint): string {
44+
const decoded = useDecoded([encoded]);
45+
// Replace .stark with .brother
46+
if (decoded.endsWith('.stark')) {
47+
return decoded.replace('.stark', '.brother');
48+
}
49+
// If no suffix, add .brother
50+
return decoded ? `${decoded}.brother` : decoded;
51+
}
52+
53+
/**
54+
* Get the Brother ID contract address for the specified network
55+
* @param chainId - The Starknet chain ID
56+
* @returns The Brother ID contract address for the network
57+
*/
58+
export function getBrotherIdContract(chainId: StarknetChainId): string {
59+
switch (chainId) {
60+
case StarknetChainId.SN_MAIN:
61+
return '0x0212f1c57700f5a3913dd11efba540196aad4cf67772f7090c62709dd804fa74';
62+
default:
63+
return '0x0212f1c57700f5a3913dd11efba540196aad4cf67772f7090c62709dd804fa74'; // Default to mainnet address
64+
}
65+
}
66+
67+
/**
68+
* Interface representing a Brother domain profile
69+
* @property name - The domain name without .brother suffix
70+
* @property resolver - The address that resolves to this domain
71+
* @property tokenId - The unique identifier of the domain NFT
72+
* @property expiryDate - Unix timestamp when the domain expires
73+
* @property lastTransferTime - Unix timestamp of the last transfer
74+
*/
75+
export interface BrotherProfile {
76+
name: string;
77+
resolver: string;
78+
tokenId: string;
79+
expiryDate: number;
80+
lastTransferTime: number;
81+
}
82+
83+
/**
84+
* Class providing methods to interact with Brother Identity contracts.
85+
*
86+
* This implementation uses the same domain encoding and decoding logic as StarknetId,
87+
* allowing for consistent handling of domain names between the two systems.
88+
* The encoding/decoding functions (encodeBrotherDomain/decodeBrotherDomain) are direct
89+
* adaptations of StarknetId's useEncoded/useDecoded functions to work with .brother domains.
90+
*/
91+
export class BrotherId {
92+
/**
93+
* Gets the primary Brother domain name for an address
94+
* @param address - The address to get the domain for
95+
* @param BrotherIdContract - Optional contract address
96+
* @returns The domain name with .brother suffix
97+
*/
98+
async getBrotherName(address: BigNumberish, BrotherIdContract?: string) {
99+
return BrotherId.getBrotherName(
100+
// After Mixin, this is ProviderInterface
101+
(<unknown>this) as ProviderInterface,
102+
address,
103+
BrotherIdContract
104+
);
105+
}
106+
107+
/**
108+
* Gets the address associated with a Brother domain name
109+
* @param name - The domain name (with or without .brother suffix)
110+
* @param BrotherIdContract - Optional contract address
111+
* @returns The resolver address for the domain
112+
*/
113+
public async getAddressFromBrotherName(
114+
name: string,
115+
BrotherIdContract?: string
116+
): Promise<string> {
117+
return BrotherId.getAddressFromBrotherName(
118+
// After Mixin, this is ProviderInterface
119+
(<unknown>this) as ProviderInterface,
120+
name,
121+
BrotherIdContract
122+
);
123+
}
124+
125+
/**
126+
* Gets the complete profile information for a Brother domain
127+
* @param address - The address to get the profile for
128+
* @param BrotherIdContract - Optional contract address
129+
* @returns The complete Brother profile information
130+
*/
131+
async getBrotherProfile(address: BigNumberish, BrotherIdContract?: string) {
132+
return BrotherId.getBrotherProfile(
133+
// After Mixin, this is ProviderInterface
134+
(<unknown>this) as ProviderInterface,
135+
address,
136+
BrotherIdContract
137+
);
138+
}
139+
140+
/**
141+
* Static implementation of getBrotherName
142+
* @param provider - The provider interface
143+
* @param address - The address to get the domain for
144+
* @param BrotherIdContract - Optional contract address
145+
* @returns The domain name with .brother suffix
146+
*/
147+
static async getBrotherName(
148+
provider: ProviderInterface,
149+
address: BigNumberish,
150+
BrotherIdContract?: string
151+
): Promise<string> {
152+
const chainId = await provider.getChainId();
153+
const contract = BrotherIdContract ?? getBrotherIdContract(chainId);
154+
155+
try {
156+
const primaryDomain = await provider.callContract({
157+
contractAddress: contract,
158+
entrypoint: 'getPrimary',
159+
calldata: CallData.compile({
160+
user: address,
161+
}),
162+
});
163+
164+
if (!primaryDomain[0] || primaryDomain[0] === '0x0') {
165+
throw Error('Brother name not found');
166+
}
167+
168+
const encodedDomain = BigInt(primaryDomain[0]);
169+
return decodeBrotherDomain(encodedDomain);
170+
} catch (e) {
171+
if (e instanceof Error && e.message === 'Brother name not found') {
172+
throw e;
173+
}
174+
throw Error('Could not get brother name');
175+
}
176+
}
177+
178+
/**
179+
* Static implementation of getAddressFromBrotherName
180+
* @param provider - The provider interface
181+
* @param name - The domain name
182+
* @param BrotherIdContract - Optional contract address
183+
* @returns The resolver address
184+
*/
185+
static async getAddressFromBrotherName(
186+
provider: ProviderInterface,
187+
name: string,
188+
BrotherIdContract?: string
189+
): Promise<string> {
190+
const brotherName = name.endsWith('.brother') ? name : `${name}.brother`;
191+
192+
if (!isBrotherDomain(brotherName)) {
193+
throw new Error('Invalid domain, must be a valid .brother domain');
194+
}
195+
196+
const chainId = await provider.getChainId();
197+
const contract = BrotherIdContract ?? getBrotherIdContract(chainId);
198+
199+
try {
200+
const domainDetails = await provider.callContract({
201+
contractAddress: contract,
202+
entrypoint: 'get_details_by_domain',
203+
calldata: CallData.compile({
204+
domain: encodeBrotherDomain(brotherName),
205+
}),
206+
});
207+
208+
if (!domainDetails[0] || domainDetails[1] === '0x0') {
209+
throw Error('Could not get address from brother name');
210+
}
211+
212+
return domainDetails[1]; // resolver address
213+
} catch {
214+
throw Error('Could not get address from brother name');
215+
}
216+
}
217+
218+
/**
219+
* Static implementation of getBrotherProfile
220+
* @param provider - The provider interface
221+
* @param address - The address to get the profile for
222+
* @param BrotherIdContract - Optional contract address
223+
* @returns The complete Brother profile
224+
*/
225+
static async getBrotherProfile(
226+
provider: ProviderInterface,
227+
address: BigNumberish,
228+
BrotherIdContract?: string
229+
): Promise<BrotherProfile> {
230+
const chainId = await provider.getChainId();
231+
const contract = BrotherIdContract ?? getBrotherIdContract(chainId);
232+
233+
try {
234+
const primaryDomain = await provider.callContract({
235+
contractAddress: contract,
236+
entrypoint: 'getPrimary',
237+
calldata: CallData.compile({
238+
user: address,
239+
}),
240+
});
241+
242+
if (!primaryDomain[0] || primaryDomain[0] === '0x0') {
243+
throw Error('Brother profile not found');
244+
}
245+
246+
const encodedDomain = BigInt(primaryDomain[0]);
247+
const decodedDomain = decodeBrotherDomain(encodedDomain);
248+
const domain = decodedDomain.replace('.brother', '');
249+
250+
const domainDetails = await provider.callContract({
251+
contractAddress: contract,
252+
entrypoint: 'get_details_by_domain',
253+
calldata: CallData.compile({
254+
domain: encodeBrotherDomain(domain),
255+
}),
256+
});
257+
258+
return {
259+
name: domain,
260+
resolver: domainDetails[1],
261+
tokenId: domainDetails[2],
262+
expiryDate: parseInt(domainDetails[3], 16),
263+
lastTransferTime: parseInt(domainDetails[4], 16),
264+
};
265+
} catch (e) {
266+
if (e instanceof Error && e.message === 'Brother profile not found') {
267+
throw e;
268+
}
269+
throw Error('Could not get brother profile');
270+
}
271+
}
272+
}

src/provider/extensions/default.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { Mixin } from 'ts-mixer';
33

44
import { RpcProvider as BaseRpcProvider } from '../rpc';
55
import { StarknetId } from './starknetId';
6+
import { BrotherId } from './brotherId';
67

7-
export class RpcProvider extends Mixin(BaseRpcProvider, StarknetId) {}
8+
export class RpcProvider extends Mixin(BaseRpcProvider, StarknetId, BrotherId) {}

0 commit comments

Comments
 (0)