Skip to content

Commit

Permalink
Implement RPC call for resolving StarkNet ID to address and limit act…
Browse files Browse the repository at this point in the history
…ivation to mainnet
  • Loading branch information
piatoss3612 committed Nov 26, 2024
1 parent 428aabd commit 32e37c2
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 35 deletions.
1 change: 1 addition & 0 deletions apps/extension/src/languages/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@
"page.ledger-grant.title": "允许浏览器连接Ledger",
"page.ledger-grant.paragraph": "你需要重新检查与Ledger的连接。选择正确的应用程序,在成功连接到你的Ledger设备后,关闭此页面并重试你以前的交易(签名中)。",
"components.empty-view.text": "还没{subject}",
"components.input.recipient-input.wallet-address-only-label": "钱包地址",
"components.input.recipient-input.wallet-address-label": "钱包地址或者ICNS",
"components.input.recipient-input.wallet-address-label-ens": "钱包地址或者ENS",
"components.input.recipient-input.wallet-address-label-starknet.id": "钱包地址或者StarkNet ID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export const RecipientInput = observer<RecipientInputProps, HTMLInputElement>(
const [isAddressBookModalOpen, setIsAddressBookModalOpen] =
React.useState(false);

const isStarknetIDEnabled: boolean = (() => {
if ("isStarknetIDEnabled" in recipientConfig) {
return recipientConfig.isStarknetIDEnabled;
}

return false;
})();

const isStarknetID: boolean = (() => {
if ("isStarknetID" in recipientConfig) {
return recipientConfig.isStarknetID;
Expand All @@ -70,15 +78,18 @@ export const RecipientInput = observer<RecipientInputProps, HTMLInputElement>(
<TextInput
ref={ref}
label={intl.formatMessage({
id: "components.input.recipient-input.wallet-address-label-starknet.id",
id: isStarknetIDEnabled
? "components.input.recipient-input.wallet-address-label-starknet.id"
: "components.input.recipient-input.wallet-address-only-label",
})}
value={recipientConfig.value}
autoComplete="off"
onChange={(e) => {
let value = e.target.value;

if (
"isStarknetID" in recipientConfig &&
"isStarknetIDEnabled" in recipientConfig &&
isStarknetIDEnabled &&
value.length > 0 &&
value[value.length - 1] === "." &&
numOfCharacter(value, ".") === 1 &&
Expand Down
3 changes: 2 additions & 1 deletion packages/hooks-starknet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"peerDependencies": {
"mobx": "^6",
"mobx-utils": "^6",
"react": "^16.8.0 || ^17 || ^18"
"react": "^16.8.0 || ^17 || ^18",
"starknet": "^6"
}
}
233 changes: 201 additions & 32 deletions packages/hooks-starknet/src/tx/recipient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,125 @@ import {
import { useState } from "react";
import { Buffer } from "buffer/";
import { simpleFetch } from "@keplr-wallet/simple-fetch";
import { CallData, constants } from "starknet";

interface StarknetIDFetchData {
isFetching: boolean;
starknetHexaddress?: string;
error?: Error;
}

const networkToNamingContractAddress = {
[constants.NetworkName.SN_MAIN]:
"0x6ac597f8116f886fa1c97a23fa4e08299975ecaf6b598873ca6792b9bbfb678",
};

const basicAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789-";
const basicSizePlusOne = BigInt(basicAlphabet.length + 1);
const bigAlphabet = "这来";
const basicAlphabetSize = BigInt(basicAlphabet.length);
const bigAlphabetSize = BigInt(bigAlphabet.length);

function encodeDomain(domain: string | undefined | null): bigint[] {
if (!domain) return [BigInt(0)];

const encoded = [];
for (const subdomain of domain.replace(".stark", "").split("."))
encoded.push(encode(subdomain));
return encoded;
}

function extractStars(str: string): [string, number] {
let k = 0;
while (str.endsWith(bigAlphabet[bigAlphabet.length - 1])) {
str = str.substring(0, str.length - 1);
k += 1;
}
return [str, k];
}

function encode(decoded: string | undefined): bigint {
let encoded = BigInt(0);
let multiplier = BigInt(1);

if (!decoded) {
return encoded;
}

if (decoded.endsWith(bigAlphabet[0] + basicAlphabet[1])) {
const [str, k] = extractStars(decoded.substring(0, decoded.length - 2));
decoded = str + bigAlphabet[bigAlphabet.length - 1].repeat(2 * (k + 1));
} else {
const [str, k] = extractStars(decoded);
if (k)
decoded =
str + bigAlphabet[bigAlphabet.length - 1].repeat(1 + 2 * (k - 1));
}

for (let i = 0; i < decoded.length; i += 1) {
const char = decoded[i];
const index = basicAlphabet.indexOf(char);
const bnIndex = BigInt(basicAlphabet.indexOf(char));

if (index !== -1) {
// add encoded + multiplier * index
if (i === decoded.length - 1 && decoded[i] === basicAlphabet[0]) {
encoded += multiplier * basicAlphabetSize;
multiplier *= basicSizePlusOne;
// add 0
multiplier *= basicSizePlusOne;
} else {
encoded += multiplier * bnIndex;
multiplier *= basicSizePlusOne;
}
} else if (bigAlphabet.indexOf(char) !== -1) {
// add encoded + multiplier * (basicAlphabetSize)
encoded += multiplier * basicAlphabetSize;
multiplier *= basicSizePlusOne;
// add encoded + multiplier * index
const newid =
(i === decoded.length - 1 ? 1 : 0) + bigAlphabet.indexOf(char);
encoded += multiplier * BigInt(newid);
multiplier *= bigAlphabetSize;
}
}

return encoded;
}

function isStarknetHexaddress(address: string): boolean {
if (!address.startsWith("0x")) {
return false;
}

const hex = address.replace("0x", "");
const buf = Buffer.from(hex, "hex");
if (buf.length !== 32) {
return false;
}
if (hex.toLowerCase() !== buf.toString("hex").toLowerCase()) {
return false;
}

return true;
}

export class RecipientConfig
extends TxChainSetter
implements IRecipientConfig, IRecipientConfigWithStarknetID
{
@observable
protected _value: string = "";

// Deep equal check is required to avoid infinite re-render.
@observable.struct
protected _starknetID:
| {
networkName: string;
namingContractAddress: string;
}
| undefined = undefined;

// Key is {chain identifier}/{starknet username}
@observable.shallow
protected _starknetIDFetchDataMap = new Map<string, StarknetIDFetchData>();
Expand All @@ -45,9 +150,38 @@ export class RecipientConfig
makeObservable(this);
}

@action
setStarknetID(chainId: string) {
const split = chainId.split(":"); // `starknet:networkName`
if (split.length < 2) {
return;
}

// Only support the mainnet for now.
const networkName = split[1] as constants.NetworkName;
if (!networkName || !(networkName === constants.NetworkName.SN_MAIN)) {
return;
}

const namingContractAddress = networkToNamingContractAddress[networkName];
if (!namingContractAddress) {
return;
}

this._starknetID = {
networkName,
namingContractAddress,
};
}

protected getStarknetIDFetchData(username: string): StarknetIDFetchData {
if (!this.chainGetter.hasModularChain(this.chainId)) {
throw new Error(`Can't find chain: ${this.chainId}`);
const modularChainInfo = this.chainGetter.getModularChain(this.chainId);
if (!("starknet" in modularChainInfo)) {
throw new Error(`${this.chainId} is not starknet chain`);
}

if (!this._starknetID) {
throw new Error("Starknet ID is not set");
}

const key = `${this.chainId}/${username}`;
Expand All @@ -59,21 +193,62 @@ export class RecipientConfig
});
});

const domain = encodeDomain(username).map((v) => v.toString(10));

simpleFetch<{
addr: string;
domain_expiry: number;
}>("https://api.starknet.id", `domain_to_addr?domain=${username}`)
.then((response) => {
if (response.status !== 200) {
throw new StarknetIDIsFetchingError("Failed to fetch the address");
jsonrpc: "2.0";
result?: string[];
id: string;
error?: {
code?: number;
message?: string;
};
}>(
"https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_7/Dd-R0QOJGtrWsePbiXmZl2QSBX5nk3vD",
"",
{
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "starknet_call",
params: [
{
contract_address: this._starknetID.namingContractAddress,
calldata: CallData.toHex({ domain, hint: [] }),
entry_point_selector:
"0x2e269d930f6d7ab92b15ce8ff9f5e63709391617e3465fff79ba6baf278ce60", // selector.getSelectorFromName("domain_to_address"),
},
"latest",
],
}),
signal: new AbortController().signal,
}
)
.then((resp) => {
if (resp.data.error && resp.data.error.message) {
throw new StarknetIDIsFetchingError(resp.data.error.message);
}

const data = resp.data.result;
if (!data) {
throw new StarknetIDIsFetchingError("no address found");
}

const data = response.data;
const addr = data.addr;
if (!addr) {
const rawHexAddr = data[0];
if (rawHexAddr === "0x0") {
throw new StarknetIDIsFetchingError("no address found");
}

const addr = "0x" + rawHexAddr.replace("0x", "").padStart(64, "0");

if (!isStarknetHexaddress(addr)) {
throw new InvalidHexError("Invalid hex address for chain");
}

runInAction(() => {
this._starknetIDFetchDataMap.set(key, {
isFetching: false,
Expand All @@ -94,15 +269,23 @@ export class RecipientConfig
return this._starknetIDFetchDataMap.get(key) ?? { isFetching: false };
}

get isStarknetIDEnabled(): boolean {
return !!this._starknetID;
}

@computed
get isStarknetID(): boolean {
const parsed = this.value.trim().split(".");
return parsed.length > 1 && parsed[parsed.length - 1] === "stark";
if (this.isStarknetIDEnabled) {
const parsed = this.value.trim().split(".");
return parsed.length > 1 && parsed[parsed.length - 1] === "stark";
}

return false;
}

@computed
get isStarknetIDFetching(): boolean {
if (!this.isStarknetID) {
if (!this.isStarknetIDEnabled || !this.isStarknetID) {
return false;
}

Expand All @@ -121,7 +304,7 @@ export class RecipientConfig
throw new Error("Chain doesn't support the starknet");
}

if (this.isStarknetID) {
if (this.isStarknetIDEnabled && this.isStarknetID) {
try {
return (
this.getStarknetIDFetchData(rawRecipient).starknetHexaddress || ""
Expand All @@ -144,7 +327,7 @@ export class RecipientConfig
};
}

if (this.isStarknetID) {
if (this.isStarknetIDEnabled && this.isStarknetID) {
try {
const fetched = this.getStarknetIDFetchData(rawRecipient);

Expand Down Expand Up @@ -180,27 +363,12 @@ export class RecipientConfig
}
}

if (!rawRecipient.startsWith("0x")) {
if (!isStarknetHexaddress(rawRecipient)) {
return {
error: new InvalidHexError("Invalid hex address for chain"),
};
}

{
const hex = rawRecipient.replace("0x", "");
const buf = Buffer.from(hex, "hex");
if (buf.length !== 32) {
return {
error: new InvalidHexError("Invalid hex address for chain"),
};
}
if (hex.toLowerCase() !== buf.toString("hex").toLowerCase()) {
return {
error: new InvalidHexError("Invalid hex address for chain"),
};
}
}

return {};
}

Expand All @@ -220,6 +388,7 @@ export const useRecipientConfig = (
) => {
const [config] = useState(() => new RecipientConfig(chainGetter, chainId));
config.setChain(chainId);
config.setStarknetID(chainId);

return config;
};
1 change: 1 addition & 0 deletions packages/hooks-starknet/src/tx/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface IRecipientConfig extends ITxChainSetter {
}

export interface IRecipientConfigWithStarknetID extends IRecipientConfig {
readonly isStarknetIDEnabled: boolean;
readonly isStarknetID: boolean;
readonly starknetExpectedDomain: string;
readonly isStarknetIDFetching: boolean;
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8886,6 +8886,7 @@ __metadata:
mobx: ^6
mobx-utils: ^6
react: ^16.8.0 || ^17 || ^18
starknet: ^6
languageName: unknown
linkType: soft

Expand Down

0 comments on commit 32e37c2

Please sign in to comment.